/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.sms;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nl.strohalm.cyclos.dao.sms.SmsMailingDAO;
import nl.strohalm.cyclos.entities.accounts.Account;
import nl.strohalm.cyclos.entities.accounts.AccountStatus;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.customization.fields.CustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.entities.sms.SmsMailing;
import nl.strohalm.cyclos.entities.sms.SmsMailingQuery;
import nl.strohalm.cyclos.scheduling.polling.SmsMailingSendingPollingTask;
import nl.strohalm.cyclos.services.accounts.AccountDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldService;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.elements.MemberServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.MessageResolver;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import org.apache.commons.collections.CollectionUtils;
/**
* Service implementation for sms mailings
*
* @author luis
*/
public class SmsMailingServiceImpl implements SmsMailingServiceLocal {
private class VariableEntry {
String key;
String value;
public VariableEntry(final String key, final String value) {
this.key = key;
this.value = value;
}
}
private SmsMailingDAO smsMailingDao;
private SettingsServiceLocal settingsService;
private MemberServiceLocal memberService;
private MemberCustomFieldService memberCustomFieldService;
private ApplicationServiceLocal applicationService;
private MessageResolver messageResolver;
private FetchServiceLocal fetchService;
private AccountServiceLocal accountService;
private CustomFieldHelper customFieldHelper;
@Override
public Map<String, String> getSmsTextVariables(final List<MemberGroup> groups) {
return getSmsTextVariables(groups, null, true);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Map<String, String> getSmsTextVariables(final List<MemberGroup> groups, final Member member, final boolean onlyVariableNames) {
final LocalSettings localSettings = settingsService.getLocalSettings();
Map<String, String> variables = new LinkedHashMap<String, String>();
// system variables
if (onlyVariableNames) {
ArrayList<VariableEntry> vars = new ArrayList<VariableEntry>();
vars.add(new VariableEntry("system_name", messageResolver.message("smsMailing.systemName")));
sortByValueAndAddToMap(vars, variables);
} else {
variables.put("system_name", localSettings.getApplicationName());
}
// Add the element variables
if (onlyVariableNames) {
ArrayList<VariableEntry> vars = new ArrayList<VariableEntry>();
vars.add(new VariableEntry("login", messageResolver.message("login.memberUsername")));
vars.add(new VariableEntry("name", messageResolver.message("member.memberName")));
vars.add(new VariableEntry("email", messageResolver.message("member.email")));
sortByValueAndAddToMap(vars, variables);
} else {
variables.putAll((Map) member.getVariableValues(localSettings));
}
// Add the account variables
if (onlyVariableNames) {
ArrayList<VariableEntry> vars = new ArrayList<VariableEntry>();
vars.add(new VariableEntry("balance", messageResolver.message("account.balance")));
vars.add(new VariableEntry("available_balance", messageResolver.message("account.availableBalance")));
vars.add(new VariableEntry("reserved_amount", messageResolver.message("account.reservedAmount")));
vars.add(new VariableEntry("credit_limit", messageResolver.message("account.creditLimit")));
vars.add(new VariableEntry("upper_credit_limit", messageResolver.message("account.upperCreditLimit")));
sortByValueAndAddToMap(vars, variables);
} else {
final List<Account> allAccounts = (List<Account>) accountService.getAccounts(member, Account.Relationships.TYPE);
final Account defaultAccount = accountService.getDefaultAccountFromList(member, allAccounts);
AccountType defaultAccountType = defaultAccount == null ? null : defaultAccount.getType();
final AccountStatus status = accountService.getCurrentStatus(new AccountDTO(member, defaultAccountType));
variables.putAll((Map) status.getVariableValues(localSettings));
}
// Add the custom field variables
if (onlyVariableNames) {
final List<MemberCustomField> allFields = memberCustomFieldService.list();
List<MemberCustomField> customFields = customFieldHelper.onlyInAllGroups(allFields, groups);
ArrayList<VariableEntry> vars = new ArrayList<VariableEntry>();
for (MemberCustomField mcf : customFields) {
vars.add(new VariableEntry(mcf.getInternalName(), mcf.getName()));
}
sortByValueAndAddToMap(vars, variables);
} else {
Collection<MemberCustomFieldValue> values = member.getCustomValues();
for (MemberCustomFieldValue fv : values) {
CustomField cf = fv.getField();
String fName = cf.getInternalName();
String fValue = fv.getValue();
variables.put(fName, fValue);
}
}
return variables;
}
@Override
public Map<String, String> getSmsTextVariables(Member member) {
member = fetchService.fetch(member, Element.Relationships.GROUP);
return getSmsTextVariables(Collections.singletonList((MemberGroup) member.getGroup()));
}
@Override
public Member nextMemberToSend(final SmsMailing mailing) {
return smsMailingDao.nextMemberToSend(mailing);
}
@Override
public SmsMailing nextToSend() {
SmsMailingQuery query = new SmsMailingQuery();
query.setUniqueResult();
query.setFinished(false);
List<SmsMailing> list = search(query);
return list.isEmpty() ? null : list.get(0);
}
@Override
public void removeMemberFromPending(final SmsMailing smsMailing, final Member member) {
smsMailingDao.removeMemberFromPending(smsMailing, member);
}
@Override
public List<SmsMailing> search(final SmsMailingQuery query) {
return smsMailingDao.search(query);
}
@Override
public SmsMailing send(final SmsMailing smsMailing) {
return doSend(smsMailing);
}
public void setAccountServiceLocal(final AccountServiceLocal accountService) {
this.accountService = accountService;
}
public void setApplicationServiceLocal(final ApplicationServiceLocal applicationService) {
this.applicationService = applicationService;
}
public void setCustomFieldHelper(final CustomFieldHelper customFieldHelper) {
this.customFieldHelper = customFieldHelper;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setMemberCustomFieldServiceLocal(final MemberCustomFieldServiceLocal memberCustomFieldService) {
this.memberCustomFieldService = memberCustomFieldService;
}
public void setMemberServiceLocal(final MemberServiceLocal memberService) {
this.memberService = memberService;
}
public void setMessageResolver(final MessageResolver messageResolver) {
this.messageResolver = messageResolver;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
public void setSmsMailingDao(final SmsMailingDAO smsMailingDao) {
this.smsMailingDao = smsMailingDao;
}
@Override
public void validate(final SmsMailing smsMailing, final boolean isMemberRequired) throws ValidationException {
if (isMemberRequired && CollectionUtils.isNotEmpty(smsMailing.getGroups())) {
throw new ValidationException();
}
getValidator(isMemberRequired).validate(smsMailing);
validateVariables(smsMailing, isMemberRequired);
}
private SmsMailing doSend(SmsMailing smsMailing) {
validate(smsMailing, smsMailing.isSingleMember());
smsMailing.setBy(LoggedUser.element());
smsMailing.setDate(Calendar.getInstance());
smsMailing.setSentSms(0);
smsMailing = smsMailingDao.insert(smsMailing);
// Send each SMS
if (smsMailing.isSingleMember() || !CollectionUtils.isEmpty(smsMailing.getGroups())) {
final MemberCustomField smsCustomField = settingsService.getSmsCustomField();
if (smsCustomField == null) {
throw new IllegalStateException("No custom field was set as SMS field under local settings");
}
smsMailingDao.assignUsersToSend(smsMailing, smsCustomField);
}
applicationService.awakePollingTaskOnTransactionCommit(SmsMailingSendingPollingTask.class);
return smsMailing;
}
private Validator getValidator(final boolean isMemberRequired) {
final Validator validator = new Validator("smsMailing");
validator.property("text").required().maxLength(160);
if (isMemberRequired) {
validator.property("member").required().add(new PropertyValidation() {
private static final long serialVersionUID = -20792899778722444L;
@Override
public ValidationError validate(final Object object, final Object property, final Object value) {
// Ensure the member has a mobile phone set
final Member member = (Member) value;
if (member == null) {
return null;
}
final MemberCustomField smsCustomField = settingsService.getSmsCustomField();
if (memberService.hasValueForField(member, smsCustomField)) {
return null;
}
return new ValidationError("smsMailing.error.noMobilePhone");
}
});
}
return validator;
}
private void sortByValueAndAddToMap(final ArrayList<VariableEntry> vars, final Map<String, String> variables) {
Collections.sort(vars, new Comparator<VariableEntry>() {
@Override
public int compare(final VariableEntry o1, final VariableEntry o2) {
return o1.value.compareTo(o2.value);
}
});
for (VariableEntry variableEntry : vars) {
variables.put(variableEntry.key, variableEntry.value);
}
}
@SuppressWarnings("unchecked")
private void validateVariables(final SmsMailing smsMailing, final boolean isMemberRequired) {
// Validate variables
String text = smsMailing.getText();
// The only variables that can appear are the allowed:
Map<String, String> variables = null;
if (isMemberRequired) {
variables = getSmsTextVariables(smsMailing.getMember());
} else {
variables = getSmsTextVariables(new ArrayList<MemberGroup>(smsMailing.getGroups()));
}
Set<String> parsedVariables = new HashSet<String>();
Pattern pattern = Pattern.compile("#[a-zA-Z_][a-zA-Z\\d_]*#");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
parsedVariables.add(matcher.group().replaceAll("#", ""));
}
Collection<String> unexpectedVariables = CollectionUtils.subtract(parsedVariables, variables.keySet());
if (CollectionUtils.isNotEmpty(unexpectedVariables)) {
throw new ValidationException("smsMailing.error.variableNotFound", unexpectedVariables);
}
}
}