package net.frontlinesms.data;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import net.frontlinesms.AppProperties;
import net.frontlinesms.BuildProperties;
import net.frontlinesms.FrontlineSMS;
import net.frontlinesms.FrontlineSMSConstants;
import net.frontlinesms.FrontlineUtils;
import net.frontlinesms.data.domain.Contact;
import net.frontlinesms.data.domain.Keyword;
import net.frontlinesms.data.domain.KeywordAction;
import net.frontlinesms.data.domain.FrontlineMessage;
import net.frontlinesms.data.domain.SmsInternetServiceSettings;
import net.frontlinesms.data.domain.SmsModemSettings;
import net.frontlinesms.data.repository.ContactDao;
import net.frontlinesms.data.repository.KeywordActionDao;
import net.frontlinesms.data.repository.KeywordDao;
import net.frontlinesms.data.repository.MessageDao;
import net.frontlinesms.data.repository.SmsInternetServiceSettingsDao;
import net.frontlinesms.data.repository.SmsModemSettingsDao;
import net.frontlinesms.email.EmailException;
import net.frontlinesms.email.smtp.SmtpEmailSender;
import net.frontlinesms.messaging.Provider;
import net.frontlinesms.messaging.sms.internet.SmsInternetService;
import net.frontlinesms.ui.i18n.InternationalisationUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Morgan Belkadi <morgan@frontlinesms.com>
* @author Alex Anderson <alex@frontlinesms.com>
*/
public class StatisticsManager {
private static final String I18N_KEY_STATS_CONTACTS = FrontlineSMSConstants.COMMON_CONTACTS;
private static final String I18N_KEY_STATS_KEYWORDS = FrontlineSMSConstants.COMMON_KEYWORDS;
private static final String I18N_KEY_STATS_KEYWORD_ACTIONS = FrontlineSMSConstants.COMMON_KEYWORD_ACTIONS;
private static final String I18N_KEY_STATS_LAST_SUBMISSION_DATE = "stats.data.last.submission.date";
private static final String I18N_KEY_STATS_OS = "stats.data.os";
private static final String I18N_KEY_STATS_PHONES_CONNECTED = "stats.data.phones.connected";
private static final String I18N_KEY_STATS_PHONES_DETAILS = "stats.data.phones.details";
private static final String I18N_KEY_STATS_RECEIVED_MESSAGES = FrontlineSMSConstants.COMMON_RECEIVED_MESSAGES;
private static final String I18N_KEY_STATS_RECEIVED_MESSAGES_SINCE_LAST_SUBMISSION = "stats.data.received.messages.since.last.submission";
private static final String I18N_KEY_STATS_SENT_MESSAGES = FrontlineSMSConstants.COMMON_SENT_MESSAGES;
private static final String I18N_KEY_STATS_SENT_MESSAGES_SINCE_LAST_SUBMISSION = "stats.data.sent.messages.since.last.submission";
private static final String I18N_KEY_STATS_USER_ID = "stats.data.user.id";
private static final String I18N_KEY_STATS_VERSION_NUMBER = "stats.data.version.number";
private static final String I18N_KEY_INTERNET_SERVICE_ACCOUNTS = "stats.data.smsdevice.internet.accounts";
/** Separates the i18n key from the ID keys in the {@link #statisticsList} for composite keys */
private static final String STATS_LIST_KEY_SEPARATOR = ":";
/** Separator used between different stat values in a statistics SMS message */
private static final char STATISTICS_SMS_SEPARATOR = ',';
/** Separator used between stat key and value for optional keys */
private static final char STATISTICS_SMS_OPTIONAL_KEY_VALUE_SEPARATOR = ':';
/** SMS keyword that statistics SMS will start with. This allows the FrontlineSMS's statistics
* generator to filter statistics SMS by keyword :-) */
private static final char STATISTICS_SMS_KEYWORD = '\u03A3';
private static final String PROPERTY_OS_NAME = "os.name";
private static final String PROPERTY_OS_VERSION = "os.version";
//> DATA ACCESS OBJECTS
/** Logging object */
private final Logger log = FrontlineUtils.getLogger(this.getClass());
/** Data Access Object for {@link Keyword}s */
@Autowired
private KeywordDao keywordDao;
/** Data Access Object for {@link Contact}s */
@Autowired
private ContactDao contactDao;
/** Data Access Object for {@link FrontlineMessage}s */
@Autowired
private MessageDao messageDao;
/** Data Access Object for {@link KeywordAction}s */
@Autowired
private KeywordActionDao keywordActionDao;
/** Data Access Object for {@link SmsInternetServiceSettingsDao}s */
@Autowired
private SmsInternetServiceSettingsDao smsInternetServiceSettingsDao;
/** Data Access Object for {@link SmsModemSettings}s */
@Autowired
private SmsModemSettingsDao smsModemSettingsDao;
/** List of statistics to send. */
private Map<String, String> statisticsList;
/** The email address of the user. If set, this is used as the From address of stats emails, and is included in the SMS too. */
private String userEmailAddress;
public StatisticsManager () {
this.statisticsList = new LinkedHashMap<String, String>();
}
/**
* SETTERS
*/
public void setContactDao(ContactDao contactDao) {
this.contactDao = contactDao;
}
public void setKeywordDao(KeywordDao keywordDao) {
this.keywordDao = keywordDao;
}
public void setMessageDao(MessageDao messageDao) {
this.messageDao = messageDao;
}
public void setKeywordActionDao(KeywordActionDao keywordActionDao) {
this.keywordActionDao = keywordActionDao;
}
public void setSmsInternetServiceSettingsDao(SmsInternetServiceSettingsDao smsInternetServiceSettingsDao) {
this.smsInternetServiceSettingsDao = smsInternetServiceSettingsDao;
}
public void setSmsModemSettingsDao(SmsModemSettingsDao smsModemSettingsDao) {
this.smsModemSettingsDao = smsModemSettingsDao;
}
/** @return {@link #statisticsList} */
public Map<String, String> getStatisticsList() {
return statisticsList;
}
/**
* Launches the collection of all the statistics which are trying to be sent to FLSMS
*/
public void collectData () {
log.trace("COLLECTING DATA");
this.collectVersionNumber();
this.collectUserId();
this.collectOSInfo();
this.collectLastSubmissionDate();
this.collectNumberOfContacts();
this.collectNumberOfReceivedMessages();
this.collectNumberOfSentMessages();
this.collectNumberOfKeyword();
this.collectNumberOfKeywordActions();
this.collectNumberOfRecognizedPhones();
this.collectPhonesDetails();
this.collectSmsInternetServices();
this.collectLanguage();
// Log the stats data.
log.info(getDataAsEmailString());
log.trace("FINISHED COLLECTING DATA");
}
/**
* Collects the User ID
*/
private void collectUserId() {
AppProperties appProperties = AppProperties.getInstance();
final String userId = appProperties.getUserId();
this.statisticsList.put(I18N_KEY_STATS_USER_ID, userId);
}
/**
* Collects the FrontlineSMS version number
*/
private void collectVersionNumber() {
final String version = BuildProperties.getInstance().getVersion();
this.statisticsList.put(I18N_KEY_STATS_VERSION_NUMBER, version);
}
/**
* Collects the name and version of the user's Operating System
*/
private void collectOSInfo() {
final String osInfo = System.getProperty(PROPERTY_OS_NAME) + " " + System.getProperty(PROPERTY_OS_VERSION);
this.statisticsList.put(I18N_KEY_STATS_OS, osInfo);
}
private void collectLanguage() {
// TODO we should collect this
}
/**
* Collects the date of the last actual submission
*/
private void collectLastSubmissionDate() {
final Long dateLastSubmit = AppProperties.getInstance().getLastStatisticsSubmissionDate();
String formattedDate;
if(dateLastSubmit == null || dateLastSubmit == 0) {
formattedDate = "";
} else {
formattedDate = InternationalisationUtils.getDateFormat().format(dateLastSubmit);
}
this.statisticsList.put(I18N_KEY_STATS_LAST_SUBMISSION_DATE, formattedDate);
}
/**
* Collects the total number of contacts
*/
private void collectNumberOfContacts() {
final int numberOfContacts = contactDao.getContactCount();
this.statisticsList.put(I18N_KEY_STATS_CONTACTS, String.valueOf(numberOfContacts));
}
/**
* Collects the total number of received messages
*/
private void collectNumberOfReceivedMessages() {
final int totalReceived = messageDao.getMessageCount(FrontlineMessage.Type.RECEIVED, null, null);
this.statisticsList.put(I18N_KEY_STATS_RECEIVED_MESSAGES, String.valueOf(totalReceived));
final Long lastSubmitDate = AppProperties.getInstance().getLastStatisticsSubmissionDate();
final int receivedSinceLastSubmit = messageDao.getMessageCount(FrontlineMessage.Type.RECEIVED, lastSubmitDate, null);
this.statisticsList.put(I18N_KEY_STATS_RECEIVED_MESSAGES_SINCE_LAST_SUBMISSION, String.valueOf(receivedSinceLastSubmit));
}
/**
* Collects the total number of sent messages
*/
private void collectNumberOfSentMessages() {
final int numberOfSentMessages = messageDao.getMessageCount(FrontlineMessage.Type.OUTBOUND, null, null);
this.statisticsList.put(I18N_KEY_STATS_SENT_MESSAGES, String.valueOf(numberOfSentMessages));
Long lastSubmitDate = AppProperties.getInstance().getLastStatisticsSubmissionDate();
final int numberOfSentMessagesSinceLastSubmission = messageDao.getMessageCount(FrontlineMessage.Type.OUTBOUND, lastSubmitDate , null);
this.statisticsList.put(I18N_KEY_STATS_SENT_MESSAGES_SINCE_LAST_SUBMISSION, String.valueOf(numberOfSentMessagesSinceLastSubmission));
}
/**
* Collects the total number of keywords
*/
private void collectNumberOfKeyword() {
final int numberOfKeyword = keywordDao.getTotalKeywordCount() - 1; // We don't want the blank keyword
this.statisticsList.put(I18N_KEY_STATS_KEYWORDS, String.valueOf(numberOfKeyword));
}
/**
* Collects the total number of keyword actions
*/
private void collectNumberOfKeywordActions() {
final int numberOfKeywordActions = keywordActionDao.getCount();
this.statisticsList.put(I18N_KEY_STATS_KEYWORD_ACTIONS, String.valueOf(numberOfKeywordActions));
}
/**
* Collects the number of phones recognized by FLSMS on this computer
* NB: It actually gets the number of configurations in the database
*/
private void collectNumberOfRecognizedPhones() {
final int numberOfRecognizedPhones = smsModemSettingsDao.getCount();
this.statisticsList.put(I18N_KEY_STATS_PHONES_CONNECTED, String.valueOf(numberOfRecognizedPhones));
}
/**
* Collects the details on the phones recognized by FLSMS on this computer
*/
private void collectPhonesDetails() {
StringBuilder phonesWorking = new StringBuilder();
final List<SmsModemSettings> modemsSettings = smsModemSettingsDao.getAll();
for (int i = 0 ; i < modemsSettings.size() ; ++i) {
SmsModemSettings modemSettings = modemsSettings.get(i);
if (modemSettings.getManufacturer() != null) {
if (i > 0) phonesWorking.append(", ");
phonesWorking.append(modemSettings.getManufacturer() + STATS_LIST_KEY_SEPARATOR + modemSettings.getModel());
}
}
this.statisticsList.put(I18N_KEY_STATS_PHONES_DETAILS, phonesWorking.toString());
}
/** Collects the number of {@link SmsInternetService} accounts. */
@SuppressWarnings("unchecked")
private void collectSmsInternetServices() {
Collection<SmsInternetServiceSettings> smsInternetServicesSettings = this.smsInternetServiceSettingsDao.getSmsInternetServiceAccounts();
Map<String, Integer> counts = new HashMap<String, Integer>();
for(SmsInternetServiceSettings settings : smsInternetServicesSettings) {
String className = settings.getServiceClassName();
if(!counts.containsKey(className)) {
counts.put(className, 1);
} else {
counts.put(className, counts.get(className) + 1);
}
}
for(Entry<String, Integer> e : counts.entrySet()) {
String value = Integer.toString(e.getValue());
try {
Class<SmsInternetService> serviceClass = (Class<SmsInternetService>) Class.forName(e.getKey());
Provider anna = (Provider) serviceClass.getAnnotation(Provider.class);
this.statisticsList.put(I18N_KEY_INTERNET_SERVICE_ACCOUNTS + STATS_LIST_KEY_SEPARATOR + anna.name(), value);
} catch (Exception ex) {
log.warn("Ignoring unrecognized internet service for stats: " + e.getKey(), ex);
}
}
}
public void sendStatistics(FrontlineSMS frontlineController) {
if (!sendStatisticsViaEmail()) {
sendStatisticsViaSms(frontlineController);
}
}
/**
* Actually sends an SMS containing the statistics in a short version
*/
private void sendStatisticsViaSms(FrontlineSMS frontlineController) {
String content = getDataAsSmsString();
String number = FrontlineSMSConstants.FRONTLINE_STATS_PHONE_NUMBER;
frontlineController.sendTextMessage(number, content);
}
/**
* Tries to send an e-mail containing the statistics in plain text
* @return true if the statistics were successfully sent
*/
private boolean sendStatisticsViaEmail() {
try {
SmtpEmailSender smtpEmailSender = new SmtpEmailSender(FrontlineSMSConstants.FRONTLINE_SUPPORT_EMAIL_SERVER);
smtpEmailSender.sendEmail(
FrontlineSMSConstants.FRONTLINE_STATS_EMAIL,
smtpEmailSender.getLocalEmailAddress(getUserEmailAddress(), "User " + this.statisticsList.get(I18N_KEY_STATS_USER_ID)),
"FrontlineSMS Statistics",
getStatisticsForEmail());
return true;
} catch(EmailException ex) {
log.info("Sending statistics via email failed.", ex);
return false;
}
}
/**
* Gets the statistics in a format suitable for emailing.
* @param bob {@link StringBuilder} used for compiling the body of the e-mail.
*/
private String getStatisticsForEmail() {
StringBuilder bob = new StringBuilder();
beginSection(bob, "Statistics");
bob.append(getDataAsEmailString());
endSection(bob, "Statistics");
return bob.toString();
}
/**
* Starts a section of the e-mail's body.
* Sections started with this method should be ended with {@link #endSection(StringBuilder, String)}
* @param bob The {@link StringBuilder} used for building the e-mail's body.
* @param sectionName The name of the section of the report that is being started.
*/
private static void beginSection(StringBuilder bob, String sectionName) {
bob.append("\n### Begin Section '" + sectionName + "' ###\n");
}
/**
* Ends a section of the e-mail's body.
* Sections ended with this should have been started with {@link #beginSection(StringBuilder, String)}
* @param bob The {@link StringBuilder} used for building the e-mail's body.
* @param sectionName The name of the section of the report that is being started.
*/
private static void endSection(StringBuilder bob, String sectionName) {
bob.append("### End Section '" + sectionName + "' ###\n");
}
//> USER DATA SETTER METHODS
public void setUserEmailAddress(String userEmailAddress) {
this.userEmailAddress = userEmailAddress;
}
public String getUserEmailAddress() {
return userEmailAddress;
}
//> REPORT GENERATION METHODS
/**
* Generate the text which will be sent via SMS.
* This is the {@link #STATISTICS_SMS_KEYWORD} followed by each data separated by {@link #STATISTICS_SMS_SEPARATOR}
* @return The generated String
*/
public String getDataAsSmsString() {
StringBuilder statsOutput = new StringBuilder();
statsOutput.append(this.getUserEmailAddress());
for(Entry<String, String> entry : statisticsList.entrySet()) {
statsOutput.append(STATISTICS_SMS_SEPARATOR);
// For composite values, we need the id from the key to be included in the
// SMS so we can make sense of the stat
String key = entry.getKey();
if(isCompositeKey(key)) {
int shortKeyBeginIndex = key.indexOf(STATS_LIST_KEY_SEPARATOR) + 1;
String shortKey = key.substring(shortKeyBeginIndex, Math.min(key.length(), shortKeyBeginIndex + 2));
statsOutput.append(shortKey);
statsOutput.append(STATISTICS_SMS_OPTIONAL_KEY_VALUE_SEPARATOR);
}
if (key.equals(I18N_KEY_STATS_PHONES_DETAILS)) {
statsOutput.append(this.shortenPhonesDetails(entry.getValue()));
} else {
statsOutput.append(entry.getValue());
}
}
return STATISTICS_SMS_KEYWORD + " " +
statsOutput.toString();
}
private String shortenPhonesDetails(String phonesDetails) {
StringBuilder shortenedPhonesDetails = new StringBuilder();
for (String phoneDetails : phonesDetails.split(", ")) {
// For each phone details
String [] details = phoneDetails.split(STATS_LIST_KEY_SEPARATOR);
String manufactuer = details[0];
// We take the two first characters of the Manufacturer and the whole model
// Ex.: SonyEricsson K800i > SoK800i
if (manufactuer.length() < 3) {
shortenedPhonesDetails.append(manufactuer);
} else {
shortenedPhonesDetails.append(manufactuer.substring(0, 2));
}
if (details.length > 1) {
// if there is a model (just in case there is not), we remove spaces to gain space
shortenedPhonesDetails.append(details[1]);
}
shortenedPhonesDetails.append(STATS_LIST_KEY_SEPARATOR);
}
if (shortenedPhonesDetails.length() > 0) {
shortenedPhonesDetails.deleteCharAt(shortenedPhonesDetails.length() - 1);
}
return shortenedPhonesDetails.toString();
}
/**
* Generate the text which will be sent via e-mail
* It represents each data with its full title
* @return The generated String
*/
public String getDataAsEmailString () {
String statsOutput = "";
for (Entry<String, String> entry : statisticsList.entrySet()) {
statsOutput += entry.getKey() + " = " + entry.getValue() + "\n";
}
return statsOutput;
}
public String toString () {
return getDataAsEmailString();
}
public int getReceivedMessages() {
return Integer.parseInt(this.statisticsList.get(I18N_KEY_STATS_RECEIVED_MESSAGES));
}
public int getSentMessages() {
return Integer.parseInt(this.statisticsList.get(I18N_KEY_STATS_SENT_MESSAGES));
}
//> STATIC HELPER METHODS
/** Checks if a key from {@link #statisticsList} is composite */
public static boolean isCompositeKey(String key) {
return key.indexOf(STATS_LIST_KEY_SEPARATOR) != -1;
}
/** Splits stats map key into constituent parts. */
public static String[] splitStatsMapKey(String key) {
if(!isCompositeKey(key)) {
return new String[]{key};
} else {
return key.split(STATS_LIST_KEY_SEPARATOR);
}
}
}