
org.asteriskjava.manager.internal.ManagerConnectionImpl Maven / Gradle / Ivy
/*
* Copyright 2004-2006 Stefan Reuter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.asteriskjava.manager.internal;
import static org.asteriskjava.manager.ManagerConnectionState.CONNECTED;
import static org.asteriskjava.manager.ManagerConnectionState.CONNECTING;
import static org.asteriskjava.manager.ManagerConnectionState.DISCONNECTED;
import static org.asteriskjava.manager.ManagerConnectionState.DISCONNECTING;
import static org.asteriskjava.manager.ManagerConnectionState.INITIAL;
import static org.asteriskjava.manager.ManagerConnectionState.RECONNECTING;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.asteriskjava.AsteriskVersion;
import org.asteriskjava.manager.AuthenticationFailedException;
import org.asteriskjava.manager.EventTimeoutException;
import org.asteriskjava.manager.ExpectedResponse;
import org.asteriskjava.manager.ManagerConnection;
import org.asteriskjava.manager.ManagerConnectionState;
import org.asteriskjava.manager.ManagerEventListener;
import org.asteriskjava.manager.ResponseEvents;
import org.asteriskjava.manager.SendActionCallback;
import org.asteriskjava.manager.TimeoutException;
import org.asteriskjava.manager.action.ChallengeAction;
import org.asteriskjava.manager.action.CommandAction;
import org.asteriskjava.manager.action.EventGeneratingAction;
import org.asteriskjava.manager.action.LoginAction;
import org.asteriskjava.manager.action.LogoffAction;
import org.asteriskjava.manager.action.ManagerAction;
import org.asteriskjava.manager.action.UserEventAction;
import org.asteriskjava.manager.event.ConnectEvent;
import org.asteriskjava.manager.event.DialBeginEvent;
import org.asteriskjava.manager.event.DialEvent;
import org.asteriskjava.manager.event.DisconnectEvent;
import org.asteriskjava.manager.event.ManagerEvent;
import org.asteriskjava.manager.event.ProtocolIdentifierReceivedEvent;
import org.asteriskjava.manager.event.ResponseEvent;
import org.asteriskjava.manager.response.ChallengeResponse;
import org.asteriskjava.manager.response.CommandResponse;
import org.asteriskjava.manager.response.ManagerError;
import org.asteriskjava.manager.response.ManagerResponse;
import org.asteriskjava.util.DateUtil;
import org.asteriskjava.util.SocketConnectionFacade;
import org.asteriskjava.util.internal.SocketConnectionFacadeImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Internal implemention of the ManagerConnection interface.
*
* @author srt
* @version $Id$
* @see org.asteriskjava.manager.ManagerConnectionFactory
*/
public class ManagerConnectionImpl implements ManagerConnection, Dispatcher {
private static final Logger logger = LoggerFactory.getLogger(ManagerConnectionImpl.class);
private static final int RECONNECTION_INTERVAL_1 = 50;
private static final int RECONNECTION_INTERVAL_2 = 5000;
private static final String DEFAULT_HOSTNAME = "localhost";
private static final int DEFAULT_PORT = 5038;
private static final int RECONNECTION_VERSION_INTERVAL = 500;
private static final int MAX_VERSION_ATTEMPTS = 4;
private static final Pattern SHOW_VERSION_PATTERN = Pattern.compile("^(core )?show version.*");
private static final Pattern VERSION_PATTERN_1_6 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?1\\.6[-. ].*");
private static final Pattern VERSION_PATTERN_1_8 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?1\\.8[-. ].*");
private static final Pattern VERSION_PATTERN_10 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?10[-. ].*");
private static final Pattern VERSION_PATTERN_11 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?11[-. ].*");
private static final Pattern VERSION_PATTERN_12 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?12[-. ].*");
private static final Pattern VERSION_PATTERN_13 =
Pattern.compile("^\\s*Asterisk ((SVN-branch|GIT)-)?13[-. ].*");
private static final Pattern VERSION_PATTERN_14 =
Pattern.compile("^\\s*Asterisk (GIT-)?14[-. ].*");
private static final AtomicLong idCounter = new AtomicLong(0);
private final long id;
/**
* Used to construct the internalActionId.
*/
private AtomicLong actionIdCounter = new AtomicLong(0);
/* Config attributes */
/**
* Hostname of the Asterisk server to connect to.
*/
private String hostname = DEFAULT_HOSTNAME;
/**
* TCP port to connect to.
*/
private int port = DEFAULT_PORT;
/**
* true
to use SSL for the connection, false
for a
* plain text connection.
*/
private boolean ssl = false;
/**
* The username to use for login as defined in Asterisk's
* manager.conf
.
*/
protected String username;
/**
* The password to use for login as defined in Asterisk's
* manager.conf
.
*/
protected String password;
/**
* Encoding used for transmission of strings.
*/
private Charset encoding = StandardCharsets.UTF_8;
/**
* The default timeout to wait for a ManagerResponse after sending a
* ManagerAction.
*/
private long defaultResponseTimeout = 2000;
/**
* The default timeout to wait for the last ResponseEvent after sending an
* EventGeneratingAction.
*/
private long defaultEventTimeout = 5000;
/**
* The timeout to use when connecting the the Asterisk server.
*/
private int socketTimeout = 0;
/**
* Closes the connection (and reconnects) if no input has been read for the
* given amount of milliseconds. A timeout of zero is interpreted as an
* infinite timeout.
*
* @see Socket#setSoTimeout(int)
*/
private int socketReadTimeout = 0;
/**
* true
to continue to reconnect after an authentication
* failure.
*/
private boolean keepAliveAfterAuthenticationFailure = true;
/**
* The socket to use for TCP/IP communication with Asterisk.
*/
private SocketConnectionFacade socket;
/**
* The thread that runs the reader.
*/
private Thread readerThread;
private final AtomicLong readerThreadCounter = new AtomicLong(0);
private final AtomicLong reconnectThreadCounter = new AtomicLong(0);
/**
* The reader to use to receive events and responses from asterisk.
*/
private ManagerReader reader;
/**
* The writer to use to send actions to asterisk.
*/
private ManagerWriter writer;
/**
* The protocol identifer Asterisk sends on connect wrapped into an object
* to be used as mutex.
*/
private final ProtocolIdentifierWrapper protocolIdentifier;
/**
* The version of the Asterisk server we are connected to.
*/
private AsteriskVersion version;
/**
* Contains the registered handlers that process the ManagerResponses.
*
* Key is the internalActionId of the Action sent and value the
* corresponding ResponseListener.
*/
private final Map responseListeners;
/**
* Contains the event handlers that handle ResponseEvents for the
* sendEventGeneratingAction methods.
*
* Key is the internalActionId of the Action sent and value the
* corresponding EventHandler.
*/
private final Map responseEventListeners;
/**
* Contains the event handlers that users registered.
*/
private final List eventListeners;
protected ManagerConnectionState state = INITIAL;
private String eventMask;
/**
* Creates a new instance.
*/
public ManagerConnectionImpl() {
this.id = idCounter.getAndIncrement();
this.responseListeners = new HashMap<>();
this.responseEventListeners = new HashMap<>();
this.eventListeners = new ArrayList<>();
this.protocolIdentifier = new ProtocolIdentifierWrapper();
}
// the following two methods can be overriden when running test cases to
// return a mock object
protected ManagerReader createReader(Dispatcher dispatcher, Object source) {
return new ManagerReaderImpl(dispatcher, source);
}
protected ManagerWriter createWriter() {
return new ManagerWriterImpl();
}
/**
* Sets the hostname of the asterisk server to connect to.
*
* Default is localhost
.
*
* @param hostname
* the hostname to connect to
*/
public void setHostname(String hostname) {
this.hostname = hostname;
}
/**
* Sets the port to use to connect to the asterisk server. This is the port
* specified in asterisk's manager.conf
file.
*
* Default is 5038.
*
* @param port
* the port to connect to
*/
public void setPort(int port) {
if (port <= 0) {
this.port = DEFAULT_PORT;
} else {
this.port = port;
}
}
/**
* Sets whether to use SSL.
* Default is false.
*
* @param ssl
* true
to use SSL for the connection,
* false
for a plain text connection.
* @since 0.3
*/
public void setSsl(boolean ssl) {
this.ssl = ssl;
}
/**
* Sets the username to use to connect to the asterisk server. This is the
* username specified in asterisk's manager.conf
file.
*
* @param username
* the username to use for login
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Sets the password to use to connect to the asterisk server. This is the
* password specified in Asterisk's manager.conf
file.
*
* @param password
* the password to use for login
*/
public void setPassword(String password) {
this.password = password;
}
@Override
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
/**
* Sets the time in milliseconds the synchronous method
* {@link #sendAction(ManagerAction)} will wait for a response before
* throwing a TimeoutException.
* Default is 2000.
*
* @param defaultResponseTimeout
* default response timeout in milliseconds
* @since 0.2
*/
public void setDefaultResponseTimeout(long defaultResponseTimeout) {
this.defaultResponseTimeout = defaultResponseTimeout;
}
/**
* Sets the time in milliseconds the synchronous method
* {@link #sendEventGeneratingAction(EventGeneratingAction)} will wait for a
* response and the last response event before throwing a TimeoutException.
* Default is 5000.
*
* @param defaultEventTimeout
* default event timeout in milliseconds
* @since 0.2
*/
public void setDefaultEventTimeout(long defaultEventTimeout) {
this.defaultEventTimeout = defaultEventTimeout;
}
/**
* Set to true
to try reconnecting to ther asterisk serve even
* if the reconnection attempt threw an AuthenticationFailedException.
* Default is true
.
*
* @param keepAliveAfterAuthenticationFailure
* true
to try
* reconnecting to ther asterisk serve even if the reconnection
* attempt threw an AuthenticationFailedException,
* false
otherwise.
*/
public void
setKeepAliveAfterAuthenticationFailure(boolean keepAliveAfterAuthenticationFailure) {
this.keepAliveAfterAuthenticationFailure = keepAliveAfterAuthenticationFailure;
}
/* Implementation of ManagerConnection interface */
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
@Override
public Charset getEncoding() {
return encoding;
}
public AsteriskVersion getVersion() {
return version;
}
public String getHostname() {
return hostname;
}
public int getPort() {
return port;
}
public boolean isSsl() {
return ssl;
}
public InetAddress getLocalAddress() {
return socket.getLocalAddress();
}
public int getLocalPort() {
return socket.getLocalPort();
}
public InetAddress getRemoteAddress() {
return socket.getRemoteAddress();
}
public int getRemotePort() {
return socket.getRemotePort();
}
public void registerUserEventClass(Class extends ManagerEvent> userEventClass) {
if (reader == null) {
reader = createReader(this, this);
}
reader.registerEventClass(userEventClass);
}
public void setSocketTimeout(int socketTimeout) {
this.socketTimeout = socketTimeout;
}
public void setSocketReadTimeout(int socketReadTimeout) {
this.socketReadTimeout = socketReadTimeout;
}
public synchronized void login()
throws IOException, AuthenticationFailedException, TimeoutException {
login(null);
}
public synchronized void login(String eventMask)
throws IOException, AuthenticationFailedException, TimeoutException {
if (state != INITIAL && state != DISCONNECTED) {
throw new IllegalStateException("Login may only be perfomed when in state "
+ "INITIAL or DISCONNECTED, but connection is in state " + state);
}
state = CONNECTING;
this.eventMask = eventMask;
try {
doLogin(defaultResponseTimeout, eventMask);
} finally {
if (state != CONNECTED) {
state = DISCONNECTED;
}
}
}
/**
* Does the real login, following the steps outlined below.
*
* - Connects to the asterisk server by calling {@link #connect()} if not
* already connected
*
- Waits until the protocol identifier is received but not longer than
* timeout ms.
*
- Sends a {@link ChallengeAction} requesting a challenge for authType
* MD5.
*
- When the {@link ChallengeResponse} is received a {@link LoginAction}
* is sent using the calculated key (MD5 hash of the password appended to
* the received challenge).
*
*
* @param timeout
* the maximum time to wait for the protocol identifier (in
* ms)
* @param eventMask
* the event mask. Set to "on" if all events should be
* send, "off" if not events should be sent or a combination of
* "system", "call" and "log" (separated by ',') to specify what
* kind of events should be sent.
* @throws IOException
* if there is an i/o problem.
* @throws AuthenticationFailedException
* if username or password are
* incorrect and the login action returns an error or if the MD5
* hash cannot be computed. The connection is closed in this
* case.
* @throws TimeoutException
* if a timeout occurs while waiting for the
* protocol identifier. The connection is closed in this case.
*/
protected synchronized void doLogin(long timeout, String eventMask)
throws IOException, AuthenticationFailedException, TimeoutException {
ChallengeAction challengeAction;
ManagerResponse challengeResponse;
String challenge;
String key;
LoginAction loginAction;
ManagerResponse loginResponse;
if (socket == null) {
connect();
}
synchronized (protocolIdentifier) {
if (protocolIdentifier.value == null) {
try {
protocolIdentifier.wait(timeout);
} catch (InterruptedException e) // NOPMD
{
Thread.currentThread()
.interrupt();
}
}
if (protocolIdentifier.value == null) {
disconnect();
if (reader != null && reader.getTerminationException() != null) {
throw reader.getTerminationException();
}
throw new TimeoutException("Timeout waiting for protocol identifier");
}
}
challengeAction = new ChallengeAction("MD5");
try {
challengeResponse = sendAction(challengeAction);
} catch (Exception e) {
disconnect();
throw new AuthenticationFailedException("Unable to send challenge action", e);
}
if (challengeResponse instanceof ChallengeResponse) {
challenge = ((ChallengeResponse) challengeResponse).getChallenge();
} else {
disconnect();
throw new AuthenticationFailedException(
"Unable to get challenge from Asterisk. ChallengeAction returned: "
+ challengeResponse.getMessage());
}
try {
MessageDigest md;
md = MessageDigest.getInstance("MD5");
if (challenge != null) {
md.update(challenge.getBytes(StandardCharsets.UTF_8));
}
if (password != null) {
md.update(password.getBytes(StandardCharsets.UTF_8));
}
key = ManagerUtil.toHexString(md.digest());
} catch (NoSuchAlgorithmException ex) {
disconnect();
throw new AuthenticationFailedException(
"Unable to create login key using MD5 Message Digest", ex);
}
loginAction = new LoginAction(username, "MD5", key, eventMask);
try {
loginResponse = sendAction(loginAction);
} catch (Exception e) {
disconnect();
throw new AuthenticationFailedException("Unable to send login action", e);
}
if (loginResponse instanceof ManagerError) {
disconnect();
throw new AuthenticationFailedException(loginResponse.getMessage());
}
logger.info("Successfully logged in");
version = determineVersion();
state = CONNECTED;
writer.setTargetVersion(version);
logger.info("Determined Asterisk version: " + version);
// generate pseudo event indicating a successful login
ConnectEvent connectEvent = new ConnectEvent(this);
connectEvent.setProtocolIdentifier(getProtocolIdentifier());
connectEvent.setDateReceived(DateUtil.getDate());
fireEvent(connectEvent);
}
protected AsteriskVersion determineVersion() throws IOException, TimeoutException {
int attempts = 0;
// if ("Asterisk Call Manager/1.1".equals(protocolIdentifier.value))
// {
// return AsteriskVersion.ASTERISK_1_6;
// }
while (attempts++ < MAX_VERSION_ATTEMPTS) {
final ManagerResponse showVersionFilesResponse;
final List showVersionFilesResult;
// increase timeout as output is quite large
showVersionFilesResponse = sendAction(new CommandAction("show version files pbx.c"),
defaultResponseTimeout * 2);
if (!(showVersionFilesResponse instanceof CommandResponse)) {
// return early in case of permission problems
// org.asteriskjava.manager.response.ManagerError:
// actionId='null'; message='Permission denied';
// response='Error';
// uniqueId='null'; systemHashcode=15231583
break;
}
showVersionFilesResult = ((CommandResponse) showVersionFilesResponse).getResult();
if (showVersionFilesResult != null && !showVersionFilesResult.isEmpty()) {
final String line1 = showVersionFilesResult.get(0);
if (line1 != null && line1.startsWith("File")) {
final String rawVersion;
rawVersion = getRawVersion();
if (rawVersion != null && rawVersion.startsWith("Asterisk 1.4")) {
return AsteriskVersion.ASTERISK_1_4;
}
return AsteriskVersion.ASTERISK_1_2;
} else if (line1 != null && line1.contains("No such command")) {
final ManagerResponse coreShowVersionResponse = sendAction(
new CommandAction("core show version"), defaultResponseTimeout * 2);
if (coreShowVersionResponse != null
&& coreShowVersionResponse instanceof CommandResponse) {
final List coreShowVersionResult =
((CommandResponse) coreShowVersionResponse).getResult();
if (coreShowVersionResult != null && !coreShowVersionResult.isEmpty()) {
final String coreLine = coreShowVersionResult.get(0);
if (VERSION_PATTERN_1_6.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_1_6;
} else if (VERSION_PATTERN_1_8.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_1_8;
} else if (VERSION_PATTERN_10.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_10;
} else if (VERSION_PATTERN_11.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_11;
} else if (VERSION_PATTERN_12.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_12;
} else if (VERSION_PATTERN_13.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_13;
} else if (VERSION_PATTERN_14.matcher(coreLine)
.matches()) {
return AsteriskVersion.ASTERISK_14;
}
}
}
try {
Thread.sleep(RECONNECTION_VERSION_INTERVAL);
} catch (Exception ex) {
// ingnore
} // NOPMD
} else {
// if it isn't the "no such command", break and return the
// lowest version immediately
break;
}
}
}
logger.error(
"Unable to determine asterisk version, assuming 1.6... you should expect problems to follow.");
return AsteriskVersion.ASTERISK_1_6;
}
protected String getRawVersion() {
final ManagerResponse showVersionResponse;
try {
showVersionResponse =
sendAction(new CommandAction("show version"), defaultResponseTimeout * 2);
} catch (Exception e) {
return null;
}
if (showVersionResponse instanceof CommandResponse) {
final List showVersionResult;
showVersionResult = ((CommandResponse) showVersionResponse).getResult();
if (showVersionResult != null && !showVersionResult.isEmpty()) {
return showVersionResult.get(0);
}
}
return null;
}
protected synchronized void connect() throws IOException {
logger.info("Connecting to " + hostname + ":" + port);
if (reader == null) {
logger.debug("Creating reader for " + hostname + ":" + port);
reader = createReader(this, this);
}
if (writer == null) {
logger.debug("Creating writer");
writer = createWriter();
}
logger.debug("Creating socket");
socket = createSocket();
logger.debug("Passing socket to reader");
reader.setSocket(socket);
if (readerThread == null || !readerThread.isAlive() || reader.isDead()) {
logger.debug("Creating and starting reader thread");
readerThread = new Thread(reader);
readerThread.setName("Asterisk-Java ManagerConnection-" + id + "-Reader-"
+ readerThreadCounter.getAndIncrement());
readerThread.setDaemon(true);
readerThread.start();
}
logger.debug("Passing socket to writer");
writer.setSocket(socket);
}
protected SocketConnectionFacade createSocket() throws IOException {
return new SocketConnectionFacadeImpl(hostname, port, ssl, socketTimeout, socketReadTimeout,
encoding);
}
public synchronized void logoff() throws IllegalStateException {
if (state != CONNECTED && state != RECONNECTING) {
throw new IllegalStateException("Logoff may only be perfomed when in state "
+ "CONNECTED or RECONNECTING, but connection is in state " + state);
}
state = DISCONNECTING;
if (socket != null) {
try {
sendAction(new LogoffAction());
} catch (Exception e) {
logger.warn("Unable to send LogOff action", e);
}
}
cleanup();
state = DISCONNECTED;
}
/**
* Closes the socket connection.
*/
protected synchronized void disconnect() {
if (socket != null) {
logger.info("Closing socket.");
try {
socket.close();
} catch (IOException ex) {
logger.warn("Unable to close socket: " + ex.getMessage());
}
socket = null;
}
protocolIdentifier.value = null;
}
public ManagerResponse sendAction(ManagerAction action)
throws IOException, TimeoutException, IllegalArgumentException, IllegalStateException {
return sendAction(action, defaultResponseTimeout);
}
/*
* Implements synchronous sending of "simple" actions.
*/
public ManagerResponse sendAction(ManagerAction action, long timeout)
throws IOException, TimeoutException, IllegalArgumentException, IllegalStateException {
ResponseHandlerResult result;
SendActionCallback callbackHandler;
result = new ResponseHandlerResult();
callbackHandler = new DefaultSendActionCallback(result);
synchronized (result) {
sendAction(action, callbackHandler);
// definitely return null for the response of user events
if (action instanceof UserEventAction) {
return null;
}
// only wait if we did not yet receive the response.
// Responses may be returned really fast.
if (result.getResponse() == null) {
try {
result.wait(timeout);
} catch (InterruptedException ex) {
logger.warn("Interrupted while waiting for result");
Thread.currentThread()
.interrupt();
}
}
}
// still no response?
if (result.getResponse() == null) {
throw new TimeoutException("Timeout waiting for response to " + action.getAction()
+ (action.getActionId() == null ? ""
: " (actionId: " + action.getActionId() + ")"));
}
return result.getResponse();
}
public void sendAction(ManagerAction action, SendActionCallback callback)
throws IOException, IllegalArgumentException, IllegalStateException {
final String internalActionId;
if (action == null) {
throw new IllegalArgumentException("Unable to send action: action is null.");
}
// In general sending actions is only allowed while connected, though
// there are a few exceptions, these are handled here:
if ((state == CONNECTING || state == RECONNECTING) && (action instanceof ChallengeAction
|| action instanceof LoginAction || isShowVersionCommandAction(action))) {
// when (re-)connecting challenge and login actions are ok.
} // NOPMD
else if (state == DISCONNECTING && action instanceof LogoffAction) {
// when disconnecting logoff action is ok.
} // NOPMD
else if (state != CONNECTED) {
throw new IllegalStateException("Actions may only be sent when in state "
+ "CONNECTED, but connection is in state " + state);
}
if (socket == null) {
throw new IllegalStateException(
"Unable to send " + action.getAction() + " action: socket not connected.");
}
internalActionId = createInternalActionId();
// if the callbackHandler is null the user is obviously not interested
// in the response, thats fine.
if (callback != null) {
synchronized (this.responseListeners) {
this.responseListeners.put(internalActionId, callback);
}
}
Class extends ManagerResponse> responseClass =
getExpectedResponseClass(action.getClass());
if (responseClass != null) {
reader.expectResponseClass(internalActionId, responseClass);
}
writer.sendAction(action, internalActionId);
}
boolean isShowVersionCommandAction(ManagerAction action) {
if (!(action instanceof CommandAction)) {
return false;
}
final Matcher showVersionMatcher =
SHOW_VERSION_PATTERN.matcher(((CommandAction) action).getCommand());
return showVersionMatcher.matches();
}
private Class extends ManagerResponse>
getExpectedResponseClass(Class extends ManagerAction> actionClass) {
final ExpectedResponse annotation = actionClass.getAnnotation(ExpectedResponse.class);
if (annotation == null) {
return null;
}
return annotation.value();
}
public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action)
throws IOException, EventTimeoutException, IllegalArgumentException,
IllegalStateException {
return sendEventGeneratingAction(action, defaultEventTimeout);
}
/*
* Implements synchronous sending of event generating actions.
*/
public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action, long timeout)
throws IOException, EventTimeoutException, IllegalArgumentException,
IllegalStateException {
final ResponseEventsImpl responseEvents;
final ResponseEventHandler responseEventHandler;
final String internalActionId;
if (action == null) {
throw new IllegalArgumentException("Unable to send action: action is null.");
} else if (action.getActionCompleteEventClass() == null) {
throw new IllegalArgumentException(
"Unable to send action: actionCompleteEventClass for " + action.getClass()
.getName() + " is null.");
} else if (!ResponseEvent.class.isAssignableFrom(action.getActionCompleteEventClass())) {
throw new IllegalArgumentException("Unable to send action: actionCompleteEventClass ("
+ action.getActionCompleteEventClass()
.getName()
+ ") for " + action.getClass()
.getName()
+ " is not a ResponseEvent.");
}
if (state != CONNECTED) {
throw new IllegalStateException("Actions may only be sent when in state "
+ "CONNECTED but connection is in state " + state);
}
responseEvents = new ResponseEventsImpl();
responseEventHandler =
new ResponseEventHandler(responseEvents, action.getActionCompleteEventClass());
internalActionId = createInternalActionId();
try {
// register response handler...
synchronized (this.responseListeners) {
this.responseListeners.put(internalActionId, responseEventHandler);
}
// ...and event handler.
synchronized (this.responseEventListeners) {
this.responseEventListeners.put(internalActionId, responseEventHandler);
}
synchronized (responseEvents) {
writer.sendAction(action, internalActionId);
// only wait if response has not yet arrived.
if (responseEvents.getResponse() == null || !responseEvents.isComplete()) {
try {
responseEvents.wait(timeout);
} catch (InterruptedException e) {
logger.warn("Interrupted while waiting for response events.");
Thread.currentThread()
.interrupt();
}
}
}
// still no response or not all events received and timed out?
if (responseEvents.getResponse() == null || !responseEvents.isComplete()) {
throw new EventTimeoutException(
"Timeout waiting for response or response events to " + action.getAction()
+ (action.getActionId() == null ? ""
: " (actionId: " + action.getActionId() + ")"),
responseEvents);
}
} finally {
// remove the event handler
synchronized (this.responseEventListeners) {
this.responseEventListeners.remove(internalActionId);
}
// Note: The response handler should have already been removed
// when the response was received, however we remove it here
// just in case it was never received.
synchronized (this.responseListeners) {
this.responseListeners.remove(internalActionId);
}
}
return responseEvents;
}
/**
* Creates a new unique internal action id based on the hash code of this
* connection and a sequence.
*
* @return a new internal action id
* @see ManagerUtil#addInternalActionId(String,String)
* @see ManagerUtil#getInternalActionId(String)
* @see ManagerUtil#stripInternalActionId(String)
*/
private String createInternalActionId() {
final StringBuilder sb;
sb = new StringBuilder();
sb.append(this.hashCode());
sb.append("_");
sb.append(actionIdCounter.getAndIncrement());
return sb.toString();
}
public void addEventListener(final ManagerEventListener listener) {
synchronized (this.eventListeners) {
// only add it if its not already there
if (!this.eventListeners.contains(listener)) {
this.eventListeners.add(listener);
}
}
}
public void removeEventListener(final ManagerEventListener listener) {
synchronized (this.eventListeners) {
if (this.eventListeners.contains(listener)) {
this.eventListeners.remove(listener);
}
}
}
public String getProtocolIdentifier() {
return protocolIdentifier.value;
}
public ManagerConnectionState getState() {
return state;
}
/* Implementation of Dispatcher: callbacks for ManagerReader */
/**
* This method is called by the reader whenever a {@link ManagerResponse} is
* received. The response is dispatched to the associated
* {@link SendActionCallback}.
*
* @param response
* the response received by the reader
* @see ManagerReader
*/
public void dispatchResponse(ManagerResponse response) {
final String actionId;
String internalActionId;
SendActionCallback listener;
// shouldn't happen
if (response == null) {
logger.error(
"Unable to dispatch null response. This should never happen. Please file a bug.");
return;
}
actionId = response.getActionId();
internalActionId = null;
listener = null;
if (actionId != null) {
internalActionId = ManagerUtil.getInternalActionId(actionId);
response.setActionId(ManagerUtil.stripInternalActionId(actionId));
}
if (logger.isDebugEnabled()) {
logger.debug("Dispatching response with internalActionId '" + internalActionId + "':\n"
+ response);
}
if (internalActionId != null) {
synchronized (this.responseListeners) {
listener = responseListeners.get(internalActionId);
if (listener != null) {
this.responseListeners.remove(internalActionId);
} else {
// when using the async sendAction it's ok not to register a
// callback so if we don't find a response handler thats ok
logger.debug("No response listener registered for " + "internalActionId '"
+ internalActionId + "'");
}
}
} else {
logger.error("Unable to retrieve internalActionId from response: " + "actionId '"
+ actionId + "':\n" + response);
}
if (listener != null) {
try {
listener.onResponse(response);
} catch (Exception e) {
logger.warn("Unexpected exception in response listener " + listener.getClass()
.getName(), e);
}
}
}
/**
* This method is called by the reader whenever a ManagerEvent is received.
* The event is dispatched to all registered ManagerEventHandlers.
*
* @param event
* the event received by the reader
* @see #addEventListener(ManagerEventListener)
* @see #removeEventListener(ManagerEventListener)
* @see ManagerReader
*/
public void dispatchEvent(ManagerEvent event) {
// shouldn't happen
if (event == null) {
logger.error(
"Unable to dispatch null event. This should never happen. Please file a bug.");
return;
}
dispatchLegacyEventIfNeeded(event);
if (logger.isDebugEnabled()) {
logger.debug("Dispatching event:\n" + event.toString());
}
// Some events need special treatment besides forwarding them to the
// registered eventListeners (clients)
// These events are handled here at first:
// Dispatch ResponseEvents to the appropriate responseEventListener
if (event instanceof ResponseEvent) {
ResponseEvent responseEvent;
String internalActionId;
responseEvent = (ResponseEvent) event;
internalActionId = responseEvent.getInternalActionId();
if (internalActionId != null) {
synchronized (responseEventListeners) {
ManagerEventListener listener;
listener = responseEventListeners.get(internalActionId);
if (listener != null) {
try {
listener.onManagerEvent(event);
} catch (Exception e) {
logger.warn("Unexpected exception in response event listener "
+ listener.getClass()
.getName(),
e);
}
}
}
} else {
// ResponseEvent without internalActionId:
// this happens if the same event class is used as response
// event
// and as an event that is not triggered by a Manager command
// Example: QueueMemberStatusEvent.
// logger.debug("ResponseEvent without "
// + "internalActionId:\n" + responseEvent);
} // NOPMD
}
if (event instanceof DisconnectEvent) {
// When we receive get disconnected while we are connected start
// a new reconnect thread and set the state to RECONNECTING.
if (state == CONNECTED) {
state = RECONNECTING;
// close socket if still open and remove reference to
// readerThread
// After sending the DisconnectThread that thread will die
// anyway.
cleanup();
Thread reconnectThread = new Thread(new Runnable() {
public void run() {
reconnect();
}
});
reconnectThread.setName("Asterisk-Java ManagerConnection-" + id + "-Reconnect-"
+ reconnectThreadCounter.getAndIncrement());
reconnectThread.setDaemon(true);
reconnectThread.start();
// now the DisconnectEvent is dispatched to registered
// eventListeners
// (clients) and after that the ManagerReaderThread is gone.
// So effectively we replaced the reader thread by a
// ReconnectThread.
} else {
// when we receive a DisconnectEvent while not connected we
// ignore it and do not send it to clients
return;
}
}
if (event instanceof ProtocolIdentifierReceivedEvent) {
ProtocolIdentifierReceivedEvent protocolIdentifierReceivedEvent;
String protocolIdentifier;
protocolIdentifierReceivedEvent = (ProtocolIdentifierReceivedEvent) event;
protocolIdentifier = protocolIdentifierReceivedEvent.getProtocolIdentifier();
setProtocolIdentifier(protocolIdentifier);
// no need to send this event to clients
return;
}
fireEvent(event);
}
/**
* Enro 2015-03 Workaround to continue having Legacy Events from Asterisk
* 13.
*/
private void dispatchLegacyEventIfNeeded(ManagerEvent event) {
if (event instanceof DialBeginEvent) {
DialEvent legacyEvent = new DialEvent((DialBeginEvent) event);
dispatchEvent(legacyEvent);
}
}
/**
* Notifies all {@link ManagerEventListener}s registered by users.
*
* @param event
* the event to propagate
*/
private void fireEvent(ManagerEvent event) {
synchronized (eventListeners) {
for (ManagerEventListener listener : eventListeners) {
try {
listener.onManagerEvent(event);
} catch (RuntimeException e) {
logger.warn("Unexpected exception in eventHandler " + listener.getClass()
.getName(), e);
}
}
}
}
/**
* This method is called when a {@link ProtocolIdentifierReceivedEvent} is
* received from the reader. Having received a correct protocol identifier
* is the precodition for logging in.
*
* @param identifier
* the protocol version used by the Asterisk server.
*/
private void setProtocolIdentifier(final String identifier) {
logger.info("Connected via " + identifier);
if (!"Asterisk Call Manager/1.0".equals(identifier)
&& !"Asterisk Call Manager/1.1".equals(identifier) // Asterisk
// 1.6
&& !"Asterisk Call Manager/1.2".equals(identifier) // bri
// stuffed
&& !"Asterisk Call Manager/1.3".equals(identifier) // Asterisk
// 11
&& !"Asterisk Call Manager/2.6.0".equals(identifier) // Asterisk
// 13
&& !"Asterisk Call Manager/2.7.0".equals(identifier) // Asterisk
// 13.2
&& !"Asterisk Call Manager/2.8.0".equals(identifier) // Asterisk
// > 13.5
&& !"VoIP API/AMI/2.0.0".equals(identifier) // zycoo asterisk 13
&& !"OpenPBX Call Manager/1.0".equals(identifier)
&& !"CallWeaver Call Manager/1.0".equals(identifier)
&& !(identifier != null && identifier.startsWith("Asterisk Call Manager Proxy/"))) {
logger.warn("Unsupported protocol version '" + identifier + "'. Use at your own risk.");
}
synchronized (protocolIdentifier) {
protocolIdentifier.value = identifier;
protocolIdentifier.notifyAll();
}
}
/**
* Reconnects to the asterisk server when the connection is lost.
* While keepAlive is true
we will try to reconnect.
* Reconnection attempts will be stopped when the {@link #logoff()} method
* is called or when the login after a successful reconnect results in an
* {@link AuthenticationFailedException} suggesting that the manager
* credentials have changed and keepAliveAfterAuthenticationFailure is not
* set.
* This method is called when a {@link DisconnectEvent} is received from the
* reader.
*/
private void reconnect() {
int numTries;
// try to reconnect
numTries = 0;
while (state == RECONNECTING) {
try {
if (numTries < 10) {
// try to reconnect quite fast for the firt 10 times
// this succeeds if the server has just been restarted
Thread.sleep(RECONNECTION_INTERVAL_1);
} else {
// slow down after 10 unsuccessful attempts asuming a
// shutdown of the server
Thread.sleep(RECONNECTION_INTERVAL_2);
}
} catch (InterruptedException e) {
Thread.currentThread()
.interrupt();
}
try {
connect();
try {
doLogin(defaultResponseTimeout, eventMask);
logger.info("Successfully reconnected.");
// everything is ok again, so we leave
// when successful doLogin set the state to CONNECTED so no
// need to adjust it
break;
} catch (AuthenticationFailedException e1) {
if (keepAliveAfterAuthenticationFailure) {
logger.error("Unable to log in after reconnect: " + e1.getMessage());
} else {
logger.error("Unable to log in after reconnect: " + e1.getMessage()
+ ". Giving up.");
state = DISCONNECTED;
}
} catch (TimeoutException e1) {
// shouldn't happen - but happens!
logger.error("TimeoutException while trying to log in " + "after reconnect.");
}
} catch (IOException e) {
// server seems to be still down, just continue to attempt
// reconnection
String message = e.getClass()
.getSimpleName();
if (e.getMessage() != null) {
message = e.getMessage();
}
logger.warn("Exception while trying to reconnect: " + message);
}
numTries++;
}
}
private void cleanup() {
disconnect();
this.readerThread = null;
}
@Override
public String toString() {
StringBuilder sb;
sb = new StringBuilder("ManagerConnection[");
sb.append("id='")
.append(id)
.append("',");
sb.append("hostname='")
.append(hostname)
.append("',");
sb.append("port=")
.append(port)
.append(",");
sb.append("systemHashcode=")
.append(System.identityHashCode(this))
.append("]");
return sb.toString();
}
/* Helper classes */
/**
* A simple data object to store a ManagerResult.
*/
private static class ResponseHandlerResult implements Serializable {
private static final long serialVersionUID = -306253761898854212L;
private ManagerResponse response;
public ResponseHandlerResult() {
}
public ManagerResponse getResponse() {
return this.response;
}
public void setResponse(ManagerResponse response) {
this.response = response;
}
}
/**
* A simple response handler that stores the received response in a
* ResponseHandlerResult for further processing.
*/
private static class DefaultSendActionCallback implements SendActionCallback, Serializable {
private static final long serialVersionUID = 7323183462307791394L;
private final ResponseHandlerResult result;
/**
* Creates a new instance.
*
* @param result
* the result to store the response in
*/
public DefaultSendActionCallback(ResponseHandlerResult result) {
this.result = result;
}
public void onResponse(ManagerResponse response) {
synchronized (result) {
result.setResponse(response);
result.notifyAll();
}
}
}
/**
* A combinded event and response handler that adds received events and the
* response to a ResponseEvents object.
*/
private static class ResponseEventHandler implements ManagerEventListener, SendActionCallback {
private final ResponseEventsImpl events;
private final Class> actionCompleteEventClass;
/**
* Creates a new instance.
*
* @param events
* the ResponseEventsImpl to store the events in
* @param actionCompleteEventClass
* the type of event that indicates that
* all events have been received
*/
public ResponseEventHandler(ResponseEventsImpl events, Class> actionCompleteEventClass) {
this.events = events;
this.actionCompleteEventClass = actionCompleteEventClass;
}
public void onManagerEvent(ManagerEvent event) {
synchronized (events) {
// should always be a ResponseEvent, anyway...
if (event instanceof ResponseEvent) {
ResponseEvent responseEvent;
responseEvent = (ResponseEvent) event;
events.addEvent(responseEvent);
}
// finished?
if (actionCompleteEventClass.isAssignableFrom(event.getClass())) {
events.setComplete(true);
// notify if action complete event and response have been
// received
if (events.getResponse() != null) {
events.notifyAll();
}
}
}
}
public void onResponse(ManagerResponse response) {
synchronized (events) {
events.setRepsonse(response);
if (response instanceof ManagerError) {
events.setComplete(true);
}
// finished?
// notify if action complete event and response have been
// received
if (events.isComplete()) {
events.notifyAll();
}
}
}
}
private static class ProtocolIdentifierWrapper {
String value;
}
@Override
public void deregisterEventClass(Class extends ManagerEvent> eventClass) {
if (reader == null) {
reader = createReader(this, this);
}
reader.deregisterEventClass(eventClass);
}
}