package ch.loway.oss.ari4java;
import ch.loway.oss.ari4java.tools.ARIException;
import ch.loway.oss.ari4java.generated.ActionApplications;
import ch.loway.oss.ari4java.generated.ActionAsterisk;
import ch.loway.oss.ari4java.generated.ActionBridges;
import ch.loway.oss.ari4java.generated.ActionChannels;
import ch.loway.oss.ari4java.generated.ActionDeviceStates;
import ch.loway.oss.ari4java.generated.ActionEndpoints;
import ch.loway.oss.ari4java.generated.ActionEvents;
import ch.loway.oss.ari4java.generated.ActionPlaybacks;
import ch.loway.oss.ari4java.generated.ActionRecordings;
import ch.loway.oss.ari4java.generated.ActionSounds;
import ch.loway.oss.ari4java.generated.Application;
import ch.loway.oss.ari4java.generated.Message;
import ch.loway.oss.ari4java.tools.AriCallback;
import java.io.IOException;
import java.net.URL;
import ch.loway.oss.ari4java.tools.BaseAriAction;
import ch.loway.oss.ari4java.tools.MessageQueue;
import ch.loway.oss.ari4java.tools.HttpClient;
import ch.loway.oss.ari4java.tools.RestException;
import ch.loway.oss.ari4java.tools.WsClient;
import ch.loway.oss.ari4java.tools.http.NettyHttpClient;
import ch.loway.oss.ari4java.tools.tags.EventSource;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* ARI factory and helper class
*
* @author lenz
* @author mwalton
*/
public class ARI {
private final static String ALLOWED_IN_UID = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private String appName = "";
private String url = "";
private AriVersion version;
private HttpClient httpClient;
private WsClient wsClient;
private ActionEvents liveActionEvent = null;
private AriSubscriber subscriptions = new AriSubscriber();
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
public void setWsClient(WsClient wsClient) {
this.wsClient = wsClient;
}
public void setVersion(AriVersion version) throws ARIException {
this.version = version;
}
public void setUrl(String url) {
this.url = url;
}
/**
* Returns the current ARI version.
*
* @return the ARI version currently used.
* @throws ARIException
*/
public AriVersion getVersion() throws ARIException {
return version;
}
/**
* Returns the server and port the websocket is connected to.
*
* @return the server currently being used.
*/
public String getUrl() {
return url;
}
/**
* Get the implementation for a given action interface
*
* @param klazz - the required action interface class
* @return An implementation instance
* @throws ARIException
*/
@SuppressWarnings("unchecked")
public <T> T getActionImpl(Class<T> klazz) throws ARIException {
// use the events method as we ref it for cleanup
if (klazz == ActionEvents.class) {
return (T) events();
}
BaseAriAction action = (BaseAriAction) buildConcreteImplementation(klazz);
action.setHttpClient(this.httpClient);
action.setWsClient(this.wsClient);
return (T) action;
}
/**
* Get the implementation for a given model interface
*
* @param klazz - the required model interface class
* @return An implementation instance
* @throws ARIException
*/
@SuppressWarnings("unchecked")
public <T> T getModelImpl(Class<T> klazz) throws ARIException {
return (T) buildConcreteImplementation(klazz);
}
/**
* Builds a concrete instance given an interface.
* Note that we make no assumptions on the type of objects being built.
* @param klazz
* @return the concrete implementation for that interface under the ARI in use.
* @throws ARIException
*/
private Object buildConcreteImplementation(Class klazz) throws ARIException {
if (version == null) {
throw new ARIException("API version not set");
}
Class concrete = version.builder.getClassFactory().getImplementationFor(klazz);
if (concrete == null) {
throw new ARIException("No concrete implementation in " + version.name() + " for " + klazz);
}
try {
return concrete.newInstance();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NullPointerException e) {
// Do nothing
e.printStackTrace();
}
throw new ARIException("Unable to build concrete implementation "
+ "for " + klazz.getName()
+ " in " + version.name()
);
}
/**
* Close an action object that is open for WebSocket interaction
*
* @param action - the action object
* @throws ARIException
*/
public void closeAction(Object action) throws ARIException {
if (!(action instanceof BaseAriAction)) {
throw new ARIException("Class " + action.getClass().getName() + " is not an Action implementation");
}
BaseAriAction ba = (BaseAriAction) action;
try {
ba.disconnectWs();
} catch (RestException e) {
throw new ARIException(e.getMessage());
}
}
/**
* Builds a connector object for the specified ARI version.
* If the version is set as IM_FEELING_LUCKY, then it will first try connecting,
* will detect the current ARI version and will then connect to it.
* This method uses Netty for both websocket and HTTP.
*
* As this sets everything up but does not do anything, we do not have any
* information on whether this connection is valid or not.
*
* @param url The URL of the Asterisk web server, e.g. http://10.10.5.8:8088/ - defined in http.conf
* @param user The user name (defined in ari.conf)
* @param pass The password
* @param version The reuired version
* @return a connection object
* @throws ARIException If the url is invalid, or the version of ARI is not supported.
*/
public static ARI build(String url, String app, String user, String pass, AriVersion version) throws ARIException {
if (version == AriVersion.IM_FEELING_LUCKY) {
AriVersion currentVersion = detectAriVersion(url, user, pass);
return build(url, app, user, pass, currentVersion);
} else {
try {
ARI ari = new ARI();
ari.appName = app;
NettyHttpClient hc = new NettyHttpClient();
hc.initialize(url, user, pass);
ari.setHttpClient(hc);
ari.setWsClient(hc);
ari.setVersion( version );
ari.setUrl(url);
return ari;
} catch (URISyntaxException e) {
throw new ARIException("Wrong URI format: " + url);
}
}
}
/**
* Sets the application name.
*
* @param s
*/
public void setAppName( String s ) {
this.appName = s;
}
/**
* Return the current application name.
*
* @return the appName
*/
public String getAppName() {
return this.appName;
}
/**
* Connect and detect the current ARI version.
* If the ARI version is not supported,
* will raise an excepttion as we have no bindings for it.
*
* @param url
* @param user
* @param pass
* @return the version of your server
* @throws ARIException if the version is not supported
*/
protected static AriVersion detectAriVersion( String url, String user, String pass ) throws ARIException {
String response = doHttpGet( url + "ari/api-docs/resources.json", user, pass );
String version = findVersionString( response );
return AriVersion.fromVersionString(version);
}
/**
* Runs an HTTP GET and returns the text downloaded.
*
* \TODO does it really belong here?
*
* @param urlWithParms
* @param user
* @param pwd
* @return The body of the HTTP request.
* @throws ARIException
*/
private static String doHttpGet(String urlWithParms, String user, String pwd) throws ARIException {
URL url = null;
final String UTF8 = "UTF-8";
try {
url = new URL(urlWithParms);
} catch (MalformedURLException e) {
throw new ARIException("MalformedUrlException: " + e.getMessage());
}
URLConnection uc = null;
try {
uc = url.openConnection();
} catch (IOException e) {
throw new ARIException("IOException: " + e.getMessage());
}
StringBuilder response = new StringBuilder();
try {
String userpass = user + ":" + pwd;
String basicAuth = "Basic " + javax.xml.bind.DatatypeConverter.printBase64Binary(userpass.getBytes(UTF8));
uc.setRequestProperty("Authorization", basicAuth);
InputStream is = null;
try {
is = uc.getInputStream();
} catch (IOException e) {
throw new ARIException("Cannot connect: " + e.getMessage());
}
BufferedReader buffReader = new BufferedReader(new InputStreamReader(is, UTF8));
String line = null;
try {
line = buffReader.readLine();
} catch (IOException e) {
throw new ARIException("IOException: " + e.getMessage());
}
while (line != null) {
response.append(line);
response.append('\n');
try {
line = buffReader.readLine();
} catch (IOException e) {
throw new ARIException("IOException: " + e.getMessage());
}
}
try {
buffReader.close();
} catch (IOException e) {
throw new ARIException("IOException: " + e.getMessage());
}
} catch (UnsupportedEncodingException e) {
throw new ARIException("Nobody is going to believe this: missing encoding UTF8 " + e.getMessage());
}
//System.out.println("Response: " + response.toString());
return response.toString();
}
/**
* Matches the version string out of the resources.json file.
*
* @param response
* @return a String describing the version reported from Asterisk.
* @throws ARIException
*/
private static String findVersionString(String response) throws ARIException {
Pattern p = Pattern.compile(".apiVersion.:\\s*\"(.+?)\"", Pattern.MULTILINE + Pattern.CASE_INSENSITIVE );
Matcher m = p.matcher(response);
if ( m.find() ) {
return m.group(1);
} else {
throw new ARIException( "Cound not match apiVersion " );
}
}
/**
* This operation is the opposite of a build() - to be called in the final
* clause where the ARI object is built.
*
* In any case, it is good practice to have a way to deallocate stuff like
* the websocket or any circular reference.
*/
public void cleanup() throws ARIException {
if ( liveActionEvent != null ) {
try {
closeAction(liveActionEvent);
} catch (ARIException e) {
// ignore on cleanup...
}
liveActionEvent = null;
}
destroy( wsClient );
if (wsClient != httpClient) {
destroy(httpClient);
}
wsClient = null;
httpClient = null;
}
/**
* Does the destruction of a client. In a sense, it is a reverse factory.
*
* @param client the client object
* @throws IllegalArgumentException All clients should be of a known type. Let's play it safe.
*/
private void destroy( Object client ) {
if ( client != null ) {
if ( client instanceof NettyHttpClient ) {
NettyHttpClient nhc = (NettyHttpClient) client;
nhc.destroy();
} else {
throw new IllegalArgumentException( "Unknown client object " + client );
}
}
}
/**
* In order to avoid multi-threading for users, you can get a
* MessageQueue object and poll on it for new messages.
* This makes sure you don't really need to synchonize or be worried by
* threading issues
*
* @return The MQ connected to your websocket.
* @throws ARIException
*/
public MessageQueue getWebsocketQueue() throws ARIException {
if ( liveActionEvent != null ) {
throw new ARIException( "Websocket already present" );
}
final MessageQueue q = new MessageQueue();
events().eventWebsocket( appName, new AriCallback<Message>() {
@Override
public void onSuccess(Message result) {
q.queue( result );
}
@Override
public void onFailure(RestException e) {
q.queueError("Err:" + e.getMessage());
}
});
return q;
}
/**
* Gets us a ready to use object.
*
* @return an Applications object.
*/
public ActionApplications applications() {
return (ActionApplications) setupAction(version.builder().actionApplications());
}
/**
* Gets us a ready to use object.
*
* @return an Asterisk object.
*/
public ActionAsterisk asterisk() {
return (ActionAsterisk) setupAction(version.builder().actionAsterisk());
}
/**
* Gets us a ready to use object.
*
* @return a Bridges object.
*/
public ActionBridges bridges() {
return (ActionBridges) setupAction(version.builder().actionBridges());
}
/**
* Gets us a ready to use object.
*
* @return a Channels object.
*/
public ActionChannels channels() {
return (ActionChannels) setupAction(version.builder().actionChannels());
}
/**
* Gets us a ready to use object.
*
* @return a deviceSTates object.
*/
public ActionDeviceStates deviceStates() {
return (ActionDeviceStates) setupAction(version.builder().actionDeviceStates());
}
/**
* Gets us a ready to use object.
*
* @return an Endpoints object.
*/
public ActionEndpoints endpoints() {
return (ActionEndpoints) setupAction(version.builder().actionEndpoints());
}
/**
* Gets us a ready to use object.
*
* @return an Events object.
*/
public ActionEvents events() {
if (liveActionEvent == null)
liveActionEvent = (ActionEvents) setupAction(version.builder().actionEvents());
return liveActionEvent;
}
/**
* Gets us a ready to use object.
*
* @return a Playbacks object.
*/
public ActionPlaybacks playbacks() {
return (ActionPlaybacks) setupAction(version.builder().actionPlaybacks());
}
/**
* Gets us a ready to use object.
*
* @return a Recordings object.
*/
public ActionRecordings recordings() {
return (ActionRecordings) setupAction(version.builder().actionRecordings());
}
/**
* Gets us a ready to use object.
*
* @return a Sounds object.
*/
public ActionSounds sounds() {
return (ActionSounds) setupAction(version.builder().actionSounds());
}
/**
* This code REALLY smells bad.
* Most likely we should either implement an interface, or push the clients
* to the default builder.
*
* See the getActionImpl() method here.
*
* \TODO
*
* @param a
* @return an Action object on which we'll set the default clients.
* @throws IllegalArgumentException
*/
public Object setupAction(Object a) throws IllegalArgumentException {
if (a instanceof BaseAriAction) {
BaseAriAction action = (BaseAriAction) a;
action.setHttpClient(this.httpClient);
action.setWsClient(this.wsClient);
} else {
throw new IllegalArgumentException("Object does not seem to be an Action implementation " + a.toString());
}
return a;
}
/**
* Wrapper of the Thread.sleep() to avoid exception.
*
* @param ms how long is it going to sleep.
*/
public static void sleep( long ms ) {
try {
Thread.sleep(ms);
} catch (InterruptedException e ) {
System.err.println( "Interrupted: " + e.getMessage() );
}
}
/**
* Generates a pseudo-random ID like "a4j.ZH6IA.IXEX0.TUIE8".
*
* @return the UID
*/
public static String getUID() {
StringBuilder sb = new StringBuilder(20);
sb.append("a4j");
for (int n = 0; n < 15; n++) {
if ((n % 5) == 0) {
sb.append(".");
}
int pos = (int) (Math.random() * ALLOWED_IN_UID.length());
sb.append(ALLOWED_IN_UID.charAt(pos));
}
return sb.toString();
}
/**
* Subscribes to an event source.
*
* @param m
* @return the Application object
* @throws RestException
*/
public Application subscribe( EventSource m ) throws RestException {
return subscriptions.subscribe(this, m);
}
/**
* Unsubscribes from an event source.
* @param m
* @throws RestException
*/
public void unsubscribe( EventSource m ) throws RestException {
subscriptions.unsubscribe(this, m);
}
/**
* Unsubscribes from all known subscriptions.
*
* @throws RestException
*/
public void unsubscribeAll() throws RestException {
subscriptions.unsubscribeAll(this);
}
/**
* This interface is used to go from an interface to its concrete
* implementation.
*/
public static interface ClassFactory {
public Class getImplementationFor( Class interfaceClass );
}
}