/*
* FrontlineSMS <http://www.frontlinesms.com>
* Copyright 2007, 2008 kiwanja
*
* This file is part of FrontlineSMS.
*
* FrontlineSMS is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* FrontlineSMS is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FrontlineSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package net.frontlinesms.messaging.sms.internet;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.frontlinesms.FrontlineUtils;
import net.frontlinesms.data.domain.*;
import net.frontlinesms.data.domain.FrontlineMessage.Status;
import net.frontlinesms.listener.SmsListener;
import net.frontlinesms.messaging.sms.properties.OptionalRadioSection;
import net.frontlinesms.messaging.sms.properties.OptionalSection;
import org.apache.log4j.Logger;
/**
* Abstract class containing all default information needed by
* Sms Internet Services (e.g Clickatell, IntelliSMS, etc).
*
* @author Carlos Eduardo Endler Genz
* @date 26/01/2009
*/
public abstract class AbstractSmsInternetService implements SmsInternetService {
//> CONSTANTS
/** Logging object */
private static Logger LOG = FrontlineUtils.getLogger(AbstractSmsInternetService.class);
/** Property name: use this service for sending SMS */
protected static final String PROPERTY_USE_FOR_SENDING = "common.use.for.sending";
/** Property name: use this service for receiving SMS */
protected static final String PROPERTY_USE_FOR_RECEIVING = "common.use.for.receiving";
/** Separator used while displaying the name in the UI */
protected static final String UI_NAME_SEPARATOR = "@";
//> INSTANCE PROPERTIES
/** The active thread running this service */
private SmsInternetServiceThread thread;
/** Queue of SMS messages waiting to be sent with this service */
protected ConcurrentLinkedQueue<FrontlineMessage> outbox = new ConcurrentLinkedQueue<FrontlineMessage>();
/** The SmsListener to which this phone handler should report SMS Message events. */
protected SmsListener smsListener;
/** Settings for this service */
private SmsInternetServiceSettings settings;
/** The status of this device */
private SmsInternetServiceStatus status = SmsInternetServiceStatus.DORMANT;
/** Extra info relating to the current status. */
private String statusDetail;
//> ACCESSOR METHODS
/** @return This internet service outbox. */
public ConcurrentLinkedQueue<FrontlineMessage> getOutbox() {
return outbox;
}
/** @return the settings attached to this {@link SmsInternetService} instance. */
public SmsInternetServiceSettings getSettings() {
return settings;
}
/** @param smsListener new vlue for {@link SmsListener} */
public void setSmsListener(SmsListener smsListener) {
this.smsListener = smsListener;
}
/** @return {@link #statusDetail} */
public String getStatusDetail() {
return this.statusDetail;
}
/** @return {@link #status} */
public SmsInternetServiceStatus getStatus() {
return this.status;
}
public String getServiceIdentification() {
return this.getMsisdn();
}
/**
* Set the status of this {@link SmsInternetService}, and fires an event to {@link #smsListener}
* @param status the status
* @param detail detail relating to the status
*/
protected void setStatus(SmsInternetServiceStatus status, String detail) {
if(this.status == null || !this.status.equals(status) || this.statusDetail == null || this.statusDetail.equals(detail)) {
this.status = status;
this.statusDetail = detail;
LOG.debug("Status [" + status.name()
+ (detail == null?"":": "+detail)
+ "]");
if (smsListener != null) {
smsListener.smsDeviceEvent(this, status);
}
}
}
//> OTHER METHODS
/**
* Adds the supplied message to the outbox.
*/
public void sendSMS(FrontlineMessage outgoingMessage) {
LOG.trace("ENTER");
outgoingMessage.setStatus(Status.PENDING);
outgoingMessage.setSenderMsisdn(getMsisdn());
outbox.add(outgoingMessage);
if (smsListener != null) {
smsListener.outgoingMessageEvent(this, outgoingMessage);
}
LOG.debug("Message added to outbox. Size is [" + outbox.size() + "]");
LOG.trace("EXIT");
}
/**
* Sets a property in {@link #settings}.
* @param key
* @param value
*/
protected void setProperty(String key, Object value) {
this.settings.set(key, value);
}
/**
* @param key The key of the property
* @param clazz The class of the property's value
* @param <T> The class of the property's value
* @return The property value, either the one stored on db (if any) or the default value.
*/
@SuppressWarnings("unchecked")
protected <T extends Object> T getPropertyValue(String key, Class<T> clazz) {
T defaultValue = (T) getValue(key, getPropertiesStructure());
if (defaultValue == null) throw new IllegalArgumentException("No default value could be found for key: " + key);
SmsInternetServiceSettingValue setValue = this.settings.get(key);
if(setValue == null) return defaultValue;
else return (T) SmsInternetServiceSettings.fromValue(defaultValue, setValue);
}
/** Stop this service from running */
public void stopRunning() {
LOG.trace("ENTER");
setStatus(SmsInternetServiceStatus.DISCONNECTED, null);
this.thread.running = false;
LOG.trace("EXIT");
}
/**
* Initialise the service using the supplied properties.
* @see SmsInternetService#setSettings(SmsInternetServiceSettings)
*/
public void setSettings(SmsInternetServiceSettings settings) {
this.settings = settings;
}
/** Starts this service. */
public synchronized void startThisThing() {
try {
setStatus(SmsInternetServiceStatus.CONNECTING, null);
init();
SmsInternetServiceThread newThread = new SmsInternetServiceThread(this);
this.thread = newThread;
newThread.start();
} catch(SmsInternetServiceInitialisationException ex) {
LOG.error("There was an error starting SMS Internet Service.", ex);
// stopThisThing() updates the status to a meaningless "not connected" message, so
// we need to keep the old, meaningul message from before
SmsInternetServiceStatus status = getStatus();
String statusDetail = getStatusDetail();
this.stopThisThing();
this.setStatus(status, statusDetail);
}
}
/** Re-connects this service. */
public void restartThisThing() {
this.stopThisThing();
this.startThisThing();
}
/** Stop this service from running */
public void stopThisThing() {
deinit();
if(this.thread != null) this.thread.running = false;
}
private class SmsInternetServiceThread extends Thread {
/** Indicates whether this {@link SmsInternetServiceThread} is running. */
protected boolean running;
SmsInternetServiceThread(AbstractSmsInternetService owner) {
super(owner.getClass().getSimpleName() + " :: " + owner.getIdentifier());
}
/**
* Sends and receives SMS messages.
*/
public void run() {
LOG.trace("ENTER");
running = true;
while (running) {
boolean sleep = true;
if (isConnected() && isUseForSending()) {
FrontlineMessage m = outbox.poll();
if (m != null) {
LOG.debug("Sending message [" + m.toString() + "]");
long startTime = System.currentTimeMillis();
sendSmsDirect(m);
LOG.debug("Send messages took [" + (System.currentTimeMillis() - startTime) + "]");
sleep = false;
}
}
if (running && isConnected() && isUseForReceiving()) {
LOG.debug("Receiving messages...");
try {
long startTime = System.currentTimeMillis();
receiveSms();
LOG.debug("Receiving messages took " + (System.currentTimeMillis() - startTime) + "ms");
} catch (SmsInternetServiceReceiveException e) {
LOG.error("Failed to receive messages.", e);
// Should this really be a status?
setStatus(SmsInternetServiceStatus.RECEIVING_FAILED, null);
}
}
// TODO verify delivery reports?
// If this thread is still running, we should have a little snooze
if (running) {
if (sleep) FrontlineUtils.sleep_ignoreInterrupts(5000); /* 5 seconds */
else FrontlineUtils.sleep_ignoreInterrupts(100); /* 0.1 seconds */
}
}
LOG.trace("EXIT");
}
}
//> ABSTRACT METHODS
/**
* Initialise all settings related to this service's {@link SmsInternetServiceThread}
* @throws SmsInternetServiceInitialisationException
*/
protected abstract void init() throws SmsInternetServiceInitialisationException;
/** De-initialise all settings related to this service's {@link SmsInternetServiceThread} */
protected abstract void deinit();
/**
* Send an SMS message using this phone handler.
* @param message The message to be sent.
*/
protected abstract void sendSmsDirect(FrontlineMessage message);
/**
* Attempt to receive SMS messages from this service.
* @throws SmsInternetServiceReceiveException If there was a problem receiving SMS
*/
protected abstract void receiveSms() throws SmsInternetServiceReceiveException;
//> STATIC HELPER METHODS
/**
* Deep-searches nested maps for a propertt's value. Maps may be nested as values
* inside other maps by wrapping them in either an {@link OptionalSection} or an
* {@link OptionalRadioSection}.
* @param key
* @param map
* @return
*/
@SuppressWarnings("unchecked")
static Object getValue(String key, Map<String, Object> map) {
if (map == null) {
// TODO when would map be null? perhaps we should just be clear that the result is undefined when this is the case?
return null;
} else if (map.containsKey(key)) {
return map.get(key);
} else {
for(Object mapValue : map.values()) {
if(mapValue instanceof OptionalSection) {
Object value = getValue(key, ((OptionalSection)mapValue).getDependencies());
if(value != null) return value;
} else if(mapValue instanceof OptionalRadioSection) {
Collection<LinkedHashMap<String, Object>> dependencies = ((OptionalRadioSection)mapValue).getAllDependencies();
for(LinkedHashMap<String, Object> dependencyMap : dependencies) {
Object value = getValue(key, dependencyMap);
if(value != null) return value;
}
}
}
}
return null;
}
}