/*
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.access;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import nl.strohalm.cyclos.dao.access.ChannelDAO;
import nl.strohalm.cyclos.dao.access.ChannelPrincipalDAO;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.Channel.Credentials;
import nl.strohalm.cyclos.entities.access.Channel.Principal;
import nl.strohalm.cyclos.entities.access.ChannelPrincipal;
import nl.strohalm.cyclos.entities.access.PrincipalType;
import nl.strohalm.cyclos.entities.customization.fields.CustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.PaymentRequestHandler;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.cache.Cache;
import nl.strohalm.cyclos.utils.cache.CacheCallback;
import nl.strohalm.cyclos.utils.cache.CacheListener;
import nl.strohalm.cyclos.utils.cache.CacheManager;
import nl.strohalm.cyclos.utils.validation.InvalidError;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
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;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
/**
* Implementation for channel service
* @author luis
*/
public class ChannelServiceImpl implements ChannelServiceLocal, InitializingBean, InitializingService {
private static final String ALL_KEY = "_ALL_";
private FetchServiceLocal fetchService;
private ChannelDAO channelDao;
private ChannelPrincipalDAO channelPrincipalDao;
private MemberCustomFieldServiceLocal memberCustomFieldService;
private SettingsServiceLocal settingsService;
private CacheManager cacheManager;
private PaymentRequestHandler paymentRequestHandler;
private CustomFieldHelper customFieldHelper;
@Override
public void afterPropertiesSet() throws Exception {
// Ensure that whenever the cache is updated, the payment request handler is also invalidated
getCache().addListener(new CacheListener() {
@Override
public void onCacheCleared(final Cache cache) {
paymentRequestHandler.invalidateCache();
}
@Override
public void onValueAdded(final Cache cache, final Serializable key, final Object value) {
paymentRequestHandler.invalidateCache();
}
@Override
public void onValueRemoved(final Cache cache, final Serializable key, final Object value) {
paymentRequestHandler.invalidateCache();
}
});
}
@Override
public boolean allowsPaymentRequest(final String channel) {
return !isBuiltin(channel);
}
@Override
public Set<Credentials> getPossibleCredentials(final Channel channel) {
final String internalName = channel.getInternalName();
if (Channel.WEB.equals(internalName)) {
// Main web allows only default
return EnumSet.of(Credentials.DEFAULT);
} else if (Arrays.asList(Channel.WAP1, Channel.WAP2, Channel.WEBSHOP).contains(internalName)) {
// Wap1/2, and WebShop disallows card security code
return EnumSet.of(Credentials.DEFAULT, Credentials.LOGIN_PASSWORD, Credentials.TRANSACTION_PASSWORD, Credentials.PIN);
} else if (Channel.REST.equals(internalName)) {
return EnumSet.of(Credentials.DEFAULT, Credentials.LOGIN_PASSWORD, Credentials.TRANSACTION_PASSWORD, Credentials.PIN, Credentials.CARD_SECURITY_CODE);
} else {
// The others don't allow default, as it must be a single shot, and default has 2 passwords
return EnumSet.of(Credentials.LOGIN_PASSWORD, Credentials.TRANSACTION_PASSWORD, Credentials.PIN, Credentials.CARD_SECURITY_CODE);
}
}
@Override
public Channel getSmsChannel() {
final String name = settingsService.getLocalSettings().getSmsChannelName();
return StringUtils.isEmpty(name) ? null : loadByInternalName(name);
}
@Override
public void initializeService() {
Locale locale = settingsService.getLocalSettings().getLocale();
channelDao.importNewBuiltin(locale);
}
@Override
public boolean isBuiltin(final String channel) {
try {
return Channel.listBuiltin().contains(channel);
} catch (final RuntimeException e) {
return false;
}
}
@Override
public List<Channel> list() {
return getCache().get(ALL_KEY, new CacheCallback() {
@Override
public Object retrieve() {
return channelDao.listAll(Channel.Relationships.PRINCIPALS);
}
});
}
@Override
public List<Channel> listBuiltin() {
return filterChannels(true);
}
@Override
public List<Channel> listExternal() {
final List<Channel> channels = list();
channels.remove(loadByInternalName(Channel.WEB));
return channels;
}
@Override
public List<Channel> listNonBuiltin() {
return filterChannels(false);
}
@Override
public List<Channel> listSupportingPaymentRequest() {
final List<Channel> channels = new ArrayList<Channel>();
for (final Channel channel : list()) {
if (channel.isPaymentRequestSupported()) {
channels.add(channel);
}
}
return channels;
}
@Override
public Channel load(final Long id) throws EntityNotFoundException {
return getCache().get(id, new CacheCallback() {
@Override
public Object retrieve() {
Channel channel = channelDao.load(id, Channel.Relationships.PRINCIPALS);
channel.setPrincipals(fetchService.fetch(channel.getPrincipals(), ChannelPrincipal.Relationships.CUSTOM_FIELD));
return channel;
}
});
}
@Override
public Collection<Channel> load(final Long[] ids) {
return channelDao.load(Arrays.asList(ids));
}
@Override
public Channel loadByInternalName(final String name) throws EntityNotFoundException {
return getCache().get(name, new CacheCallback() {
@Override
public Object retrieve() {
Channel channel = channelDao.loadByInternalName(name, Channel.Relationships.PRINCIPALS);
channel.setPrincipals(fetchService.fetch(channel.getPrincipals(), ChannelPrincipal.Relationships.CUSTOM_FIELD));
return channel;
}
});
}
@Override
public List<MemberCustomField> possibleCustomFieldsAsPrincipal() {
final List<MemberCustomField> allMemberFields = memberCustomFieldService.list();
final List<MemberCustomField> possible = new ArrayList<MemberCustomField>();
for (final MemberCustomField field : allMemberFields) {
if (field.getType() == CustomField.Type.STRING && field.getValidation().isUnique()) {
possible.add(field);
}
}
return possible;
}
@Override
public int remove(final Long... ids) {
try {
return channelDao.delete(ids);
} finally {
getCache().clear();
}
}
@Override
public PrincipalType resolvePrincipalType(final String principalTypeString) {
PrincipalType principalType = null;
try {
// Try by principal enum
final Principal principal = Principal.valueOf(principalTypeString);
if (principal != Principal.CUSTOM_FIELD) {
// Custom fields should be resolved by their internal name
principalType = new PrincipalType(principal);
}
} catch (final Exception e) {
// Try by custom field
final List<MemberCustomField> possibleFields = possibleCustomFieldsAsPrincipal();
final MemberCustomField customField = customFieldHelper.findByInternalName(possibleFields, principalTypeString);
if (customField != null) {
principalType = new PrincipalType(customField);
}
}
return principalType;
}
@Override
public PrincipalType resolvePrincipalType(final String channelName, final String principalTypeString) {
final Channel channel = loadByInternalName(channelName);
PrincipalType principalType = resolvePrincipalType(principalTypeString);
// Ensure the principal is supported by the channel
if (!channel.getPrincipalTypes().contains(principalType)) {
principalType = channel.getDefaultPrincipalType();
}
return principalType == null ? Channel.DEFAULT_PRINCIPAL_TYPE : principalType;
}
@Override
public Channel save(Channel channel) {
validate(channel);
// Ensure there is a default
final Collection<ChannelPrincipal> principals = channel.getPrincipals();
boolean hasDefault = false;
ChannelPrincipal user = null;
for (final ChannelPrincipal channelPrincipal : principals) {
if (channelPrincipal.getPrincipal() == Principal.USER) {
user = channelPrincipal;
}
if (channelPrincipal.isDefault()) {
hasDefault = true;
}
}
if (!hasDefault) {
// When no default, set preferentially the USER, or the first one
if (user != null) {
user.setDefault(true);
} else {
principals.iterator().next().setDefault(true);
}
}
// Ensure that the web channel has USER set, otherwise, admins wouldn't login
if (Channel.WEB.equals(channel.getInternalName())) {
if (user == null) {
user = new ChannelPrincipal();
user.setChannel(channel);
user.setPrincipal(Principal.USER);
principals.add(user);
}
}
// Ensure there's a valid credentials
final Set<Credentials> possibleCredentials = getPossibleCredentials(channel);
if (!possibleCredentials.contains(channel.getCredentials())) {
channel.setCredentials(possibleCredentials.iterator().next());
}
try {
if (channel.isTransient()) {
// Check that the internal name doesn't exists.
if (!channelDao.existsChannel(channel.getInternalName())) {
channel = channelDao.insert(channel);
} else {
throw new ValidationException("channel.internalNameAlreadyInUse");
}
} else {
// Load the current data to ensure that restrictions are enforced
final Channel current = channelDao.load(channel.getId());
// Ensure the internal name does not changes
final String internalName = current.getInternalName();
if (!allowsPaymentRequest(internalName)) {
// If payment request is not allowed, ensure the WS url is null
channel.setPaymentRequestWebServiceUrl(null);
}
channel.setPrincipals(null);
channel = channelDao.update(channel);
channelPrincipalDao.deleteAllFrom(channel);
}
// Insert the channel principals
for (final ChannelPrincipal channelPrincipal : principals) {
channelPrincipalDao.insert(channelPrincipal);
}
} finally {
getCache().clear();
}
return channel;
}
public void setCacheManager(final CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setChannelDao(final ChannelDAO channelDao) {
this.channelDao = channelDao;
}
public void setChannelPrincipalDao(final ChannelPrincipalDAO channelPrincipalDao) {
this.channelPrincipalDao = channelPrincipalDao;
}
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 setPaymentRequestHandler(final PaymentRequestHandler paymentRequestHandler) {
this.paymentRequestHandler = paymentRequestHandler;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
@Override
public void validate(final Channel channel) throws ValidationException {
final Validator validator = new Validator("channel");
validator.property("internalName").required().maxLength(50);
validator.property("displayName").required().maxLength(100);
validator.property("principals").required().add(new PropertyValidation() {
private static final long serialVersionUID = -2500914238715839704L;
@Override
@SuppressWarnings("unchecked")
public ValidationError validate(final Object object, final Object property, final Object value) {
final Collection<ChannelPrincipal> principals = (Collection<ChannelPrincipal>) value;
if (CollectionUtils.isEmpty(principals)) {
// Will already fail validation by the required
return null;
}
for (final ChannelPrincipal channelPrincipal : principals) {
if (channelPrincipal.getPrincipal() == null) {
return new RequiredError();
} else if (channelPrincipal.getPrincipal() == Principal.CUSTOM_FIELD) {
final MemberCustomField customField = fetchService.fetch(channelPrincipal.getCustomField());
if (customField == null) {
return new RequiredError();
}
if (customField.getType() != CustomField.Type.STRING) {
return new InvalidError();
}
if (!customField.getValidation().isUnique()) {
return new InvalidError();
}
}
}
return null;
}
});
final Set<Credentials> possibleCredentials = getPossibleCredentials(channel);
if (channel.getDefaultPrincipalType().getPrincipal() != Principal.CARD) {
// Ensure card security code is only possible if principal could be card
possibleCredentials.remove(Credentials.CARD_SECURITY_CODE);
}
if (possibleCredentials.size() > 1) {
// Only required if there's choice
validator.property("credentials").required().anyOf(possibleCredentials);
}
validator.validate(channel);
}
/**
* @param builtin if it's false then the built-in are removed
*/
private List<Channel> filterChannels(final boolean builtin) {
final List<Channel> channels = list();
// Remove those which are built-in
for (final Iterator<Channel> iterator = channels.iterator(); iterator.hasNext();) {
final Channel channel = iterator.next();
if (builtin && !isBuiltin(channel.getInternalName()) || !builtin && isBuiltin(channel.getInternalName())) {
iterator.remove();
}
}
return channels;
}
private Cache getCache() {
return cacheManager.getCache("cyclos.Channels");
}
}