nl.strohalm.cyclos.services.elements.ElementServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.elements.ElementServiceImpl.java

Source

/*
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.elements;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import nl.strohalm.cyclos.access.AdminAdminPermission;
import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.dao.access.UserDAO;
import nl.strohalm.cyclos.dao.access.UsernameChangeLogDAO;
import nl.strohalm.cyclos.dao.groups.GroupHistoryLogDAO;
import nl.strohalm.cyclos.dao.members.ElementDAO;
import nl.strohalm.cyclos.dao.members.NotificationPreferenceDAO;
import nl.strohalm.cyclos.dao.members.PendingEmailChangeDAO;
import nl.strohalm.cyclos.dao.members.PendingMemberDAO;
import nl.strohalm.cyclos.dao.members.RegistrationAgreementLogDAO;
import nl.strohalm.cyclos.entities.Entity;
import nl.strohalm.cyclos.entities.Relationship;
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.MemberUser;
import nl.strohalm.cyclos.entities.access.OperatorUser;
import nl.strohalm.cyclos.entities.access.PrincipalType;
import nl.strohalm.cyclos.entities.access.User;
import nl.strohalm.cyclos.entities.access.UsernameChangeLog;
import nl.strohalm.cyclos.entities.accounts.Account;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.MemberAccount;
import nl.strohalm.cyclos.entities.accounts.MemberAccountType;
import nl.strohalm.cyclos.entities.accounts.MemberGroupAccountSettings;
import nl.strohalm.cyclos.entities.accounts.cards.Card;
import nl.strohalm.cyclos.entities.accounts.loans.Loan;
import nl.strohalm.cyclos.entities.accounts.loans.LoanParameters;
import nl.strohalm.cyclos.entities.accounts.loans.LoanQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.InvoiceQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.Payment;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.ads.AdQuery;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.groups.BrokerGroup;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.GroupFilter;
import nl.strohalm.cyclos.entities.groups.GroupHistoryLog;
import nl.strohalm.cyclos.entities.groups.GroupQuery;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings;
import nl.strohalm.cyclos.entities.groups.OperatorGroup;
import nl.strohalm.cyclos.entities.members.Administrator;
import nl.strohalm.cyclos.entities.members.BrokeringQuery;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.ElementQuery;
import nl.strohalm.cyclos.entities.members.FullTextAdminQuery;
import nl.strohalm.cyclos.entities.members.FullTextElementQuery;
import nl.strohalm.cyclos.entities.members.FullTextMemberQuery;
import nl.strohalm.cyclos.entities.members.FullTextOperatorQuery;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.MemberQuery;
import nl.strohalm.cyclos.entities.members.Operator;
import nl.strohalm.cyclos.entities.members.PendingEmailChange;
import nl.strohalm.cyclos.entities.members.PendingMember;
import nl.strohalm.cyclos.entities.members.PendingMemberQuery;
import nl.strohalm.cyclos.entities.members.RegisteredMember;
import nl.strohalm.cyclos.entities.members.RegistrationAgreement;
import nl.strohalm.cyclos.entities.members.RegistrationAgreementLog;
import nl.strohalm.cyclos.entities.members.adInterests.AdInterestQuery;
import nl.strohalm.cyclos.entities.members.brokerings.Brokering;
import nl.strohalm.cyclos.entities.members.messages.Message.Type;
import nl.strohalm.cyclos.entities.members.preferences.NotificationPreference;
import nl.strohalm.cyclos.entities.members.remarks.GroupRemark;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.entities.settings.AccessSettings;
import nl.strohalm.cyclos.entities.settings.AccessSettings.UsernameGeneration;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.entities.settings.MessageSettings;
import nl.strohalm.cyclos.exceptions.MailSendingException;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.access.AccessServiceLocal;
import nl.strohalm.cyclos.services.access.ChannelServiceLocal;
import nl.strohalm.cyclos.services.access.exceptions.NotConnectedException;
import nl.strohalm.cyclos.services.accounts.AccountDateDTO;
import nl.strohalm.cyclos.services.accounts.AccountServiceLocal;
import nl.strohalm.cyclos.services.accounts.MemberAccountHandler;
import nl.strohalm.cyclos.services.accounts.cards.CardServiceLocal;
import nl.strohalm.cyclos.services.accounts.pos.PosServiceLocal;
import nl.strohalm.cyclos.services.ads.AdServiceLocal;
import nl.strohalm.cyclos.services.customization.AdminCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.customization.OperatorCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.elements.exceptions.MemberHasBalanceException;
import nl.strohalm.cyclos.services.elements.exceptions.MemberHasOpenInvoicesException;
import nl.strohalm.cyclos.services.elements.exceptions.MemberHasPendingLoansException;
import nl.strohalm.cyclos.services.elements.exceptions.RegistrationAgreementNotAcceptedException;
import nl.strohalm.cyclos.services.elements.exceptions.UsernameAlreadyInUseException;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.groups.GroupServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.preferences.PreferenceServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.services.transactions.InvoiceServiceLocal;
import nl.strohalm.cyclos.services.transactions.LoanServiceLocal;
import nl.strohalm.cyclos.services.transactions.PaymentServiceLocal;
import nl.strohalm.cyclos.services.transactions.ScheduledPaymentServiceLocal;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.ElementVO;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.HashHandler;
import nl.strohalm.cyclos.utils.MailHandler;
import nl.strohalm.cyclos.utils.MessageProcessingHelper;
import nl.strohalm.cyclos.utils.MessageResolver;
import nl.strohalm.cyclos.utils.Pair;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.RangeConstraint;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.StringHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.access.PermissionHelper;
import nl.strohalm.cyclos.utils.lock.UniqueObjectHandler;
import nl.strohalm.cyclos.utils.notifications.AdminNotificationHandler;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.validation.DelegatingValidator;
import nl.strohalm.cyclos.utils.validation.EmailValidation;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.LengthValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.UniqueError;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;

/**
 * Implementation for element service
 * @author luis
 */
public class ElementServiceImpl implements ElementServiceLocal {

    private static enum ActivationMail {
        IGNORE, THREADED, ONLINE
    }

    /**
     * Validates that the element's username is not already taken
     * @author luis
     */
    private class ExistingUsernameValidation implements GeneralValidation {
        private static final long serialVersionUID = -3358417537796698704L;

        @Override
        public ValidationError validate(final Object object) {
            String username = null;
            if (object instanceof Element) {
                username = ((Element) object).getUsername();
            } else if (object instanceof PendingMember) {
                username = ((PendingMember) object).getUsername();
            }
            final Long id = ((Entity) object).getId();

            if (StringUtils.isEmpty(username)) {
                return null;
            }
            boolean existing = false;
            if (object instanceof Operator) {
                Member member;
                if (LoggedUser.isOperator()) { // an operator modifying his own profile
                    member = (Member) LoggedUser.accountOwner();
                } else {
                    member = LoggedUser.element();
                }

                try {
                    final OperatorUser existingOperator = userDao.loadOperator(member, username);
                    existing = !existingOperator.getId().equals(id);
                } catch (final EntityNotFoundException e) {
                    // not found. ok
                }
            } else {
                // Search in Elements
                try {
                    final User existingUser = userDao.load(username);
                    existing = !existingUser.getId().equals(id);
                } catch (final EntityNotFoundException e) {
                    // not found. ok
                }
                if (!existing) {
                    // Search in PendingMembers
                    try {
                        final PendingMember pendingMember = pendingMemberDao.loadByUsername(username);
                        if (object instanceof PendingMember
                                && pendingMember.getId().equals(((PendingMember) object).getId())) {
                            // Updating the same pending member. Don't consider as existing
                            existing = false;
                        } else {
                            existing = true;
                        }
                    } catch (final EntityNotFoundException e) {
                        // not found. ok
                    }
                }
            }
            if (existing) {
                // If it got to this point, an user with the given username already exists
                return new ValidationError("createMember.error.usernameAlreadyInUse", username);
            }
            return null;
        }
    }

    private class PendingMemberEmailValidation implements PropertyValidation {
        private final PendingMember pendingMember;
        private static final long serialVersionUID = 377970183876523686L;

        private PendingMemberEmailValidation(final PendingMember pendingMember) {
            this.pendingMember = pendingMember;
        }

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            final String email = (String) value;
            LocalSettings localSettings = settingsService.getLocalSettings();
            if (localSettings.isEmailUnique()) {
                if (StringUtils.isNotEmpty(email) && pendingMemberDao.emailExists(pendingMember, email)) {
                    return new UniqueError(email);
                }
                try {
                    final Element loaded = elementDao.loadByEmail(email);
                    if (loaded != null) {
                        return new UniqueError(email);
                    }
                } catch (final EntityNotFoundException e) {
                    // Ok, no one had that e-mail
                }
            }
            return null;
        }
    }

    private class UniqueEmailValidation implements PropertyValidation {
        private static final long serialVersionUID = -1170302387628372503L;
        private final Long userId;
        private final boolean pendingToo;

        private UniqueEmailValidation(final Long userId, final boolean pendingToo) {
            this.userId = userId;
            this.pendingToo = pendingToo;
        }

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            final String email = (String) value;
            if (StringUtils.isEmpty(email)) {
                return null;
            }
            try {
                final Element loaded = elementDao.loadByEmail(email);
                if (userId == null) {
                    // member is new, not yet persisted element, so always wrong if the email already exists
                    return new UniqueError(email);
                }
                if (!loaded.getId().equals(userId)) {
                    return new UniqueError(email);
                }
            } catch (final EntityNotFoundException e) {
                // Ok, no one had that e-mail
            }
            if (pendingToo && pendingMemberDao.emailExists(null, email)) {
                // The e-mail is used by a PendingMember
                return new UniqueError(email);
            }
            return null;
        }
    }

    private MessageResolver messageResolver = new MessageResolver.NoOpMessageResolver();
    private ElementDAO elementDao;
    private GroupHistoryLogDAO groupHistoryLogDao;
    private NotificationPreferenceDAO notificationPreferenceDao;
    private UserDAO userDao;
    private AccessServiceLocal accessService;
    private AccountServiceLocal accountService;
    private AdInterestServiceLocal adInterestService;
    private AdServiceLocal adService;
    private BrokeringServiceLocal brokeringService;
    private CommissionServiceLocal commissionService;
    private ContactServiceLocal contactService;
    private AdminCustomFieldServiceLocal adminCustomFieldService;
    private MemberCustomFieldServiceLocal memberCustomFieldService;
    private OperatorCustomFieldServiceLocal operatorCustomFieldService;
    private FetchServiceLocal fetchService;
    private GroupServiceLocal groupService;
    private InvoiceServiceLocal invoiceService;
    private PaymentServiceLocal paymentService;
    private LoanServiceLocal loanService;
    private ScheduledPaymentServiceLocal scheduledPaymentService;
    private PreferenceServiceLocal preferenceService;
    private RemarkServiceLocal remarkService;
    private SettingsServiceLocal settingsService;
    private ChannelServiceLocal channelService;
    private HashHandler hashHandler;
    private MailHandler mailHandler;
    private MemberAccountHandler memberAccountHandler;
    private PendingMemberDAO pendingMemberDao;
    private PendingEmailChangeDAO pendingEmailChangeDao;
    private RegistrationAgreementLogDAO registrationAgreementLogDao;
    private CardServiceLocal cardService;
    private PosServiceLocal posService;
    private PermissionServiceLocal permissionService;
    private UsernameChangeLogDAO usernameChangeLogDao;
    private AdminNotificationHandler adminNotificationHandler;
    private MemberNotificationHandler memberNotificationHandler;
    private CustomFieldHelper customFieldHelper;
    private UniqueObjectHandler uniqueObjectHandler;

    @Override
    public void acceptAgreement(final String remoteAddress) {
        Member member = LoggedUser.member();
        MemberGroup group = member.getMemberGroup();

        RegistrationAgreement registrationAgreement = group.getRegistrationAgreement();
        if (registrationAgreement == null) {
            throw new ValidationException();
        }

        createAgreementLog(remoteAddress, member, registrationAgreement);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public boolean applyQueryRestrictions(final FullTextElementQuery query) {
        if (query instanceof FullTextAdminQuery) {
            // Only admins with permissions can search admins
            permissionService.permission().admin(AdminAdminPermission.ADMINS_VIEW).check();
        } else if (query instanceof FullTextOperatorQuery) {
            // Only members and operators can see other operators from the member himself
            Member member = ((FullTextOperatorQuery) query).getMember();
            if (member == null) {
                throw new ValidationException();
            }
            permissionService.permission(member).member().operator().check();
        } else if (query instanceof FullTextMemberQuery) {
            // For members, just enforce the visible groups
            Collection<Group> visibleGroups = (Collection) permissionService.getVisibleMemberGroups(false);
            if (visibleGroups.isEmpty()) {
                return false;
            }
            Collection<Group> queryGroups = (Collection<Group>) query.getGroups();
            query.setGroups(PermissionHelper.checkSelection(visibleGroups, queryGroups));
        }
        // Ensure that only enabled users will be returned
        boolean isMemberSearchingForOperators = (query instanceof FullTextOperatorQuery)
                && LoggedUser.element().equals(((FullTextOperatorQuery) query).getMember());
        if (!LoggedUser.isAdministrator() && !isMemberSearchingForOperators) {
            query.setEnabled(true);
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    @Override
    public BulkMemberActionResultVO bulkChangeMemberChannels(final FullTextMemberQuery query,
            Collection<Channel> enableChannels, Collection<Channel> disableChannels) throws ValidationException {
        validateBulkChangeChannels(query, enableChannels, disableChannels);

        // load the channels to ensure they are valid ones
        enableChannels = channelService.load(EntityHelper.toIds(enableChannels));
        disableChannels = channelService.load(EntityHelper.toIds(disableChannels));

        int changed = 0, unchanged = 0;
        // force the result type to ITERATOR to avoid load all members in memory
        query.setIterateAll();
        final List<Member> members = (List<Member>) fullTextSearch(query);
        CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
        for (Member member : members) {
            member = fetchService.fetch(member, Member.Relationships.CHANNELS, Element.Relationships.GROUP);
            boolean mustChange = false;

            for (Channel channel : enableChannels) {
                if (accessService.isChannelAllowedToBeEnabledForMember(channel, member)
                        && !member.getChannels().contains(channel)) {
                    mustChange = true;
                    member.getChannels().add(channel);
                }
            }
            Collection<Channel> memberChannels = accessService.getChannelsEnabledForMember(member);
            Collection<Channel> memberDisabledChannels = CollectionUtils.subtract(channelService.list(),
                    memberChannels);
            Collection<Channel> toDisableChannels = CollectionUtils.subtract(disableChannels,
                    memberDisabledChannels);
            if (CollectionUtils.isNotEmpty(toDisableChannels)) {
                mustChange = true;
                // at this point disableChannels contains only actually enabled member channels
                for (Channel channel : toDisableChannels) {
                    member.getChannels().remove(channel);
                }
            }

            if (mustChange) {
                changed++;
                elementDao.update(member);
            } else {
                unchanged++;
            }

            cacheCleaner.clearCache();
        }

        return new BulkMemberActionResultVO(changed, unchanged);
    }

    @Override
    @SuppressWarnings("unchecked")
    public BulkMemberActionResultVO bulkChangeMemberGroup(final FullTextMemberQuery query, MemberGroup newGroup,
            final String comments) throws ValidationException {
        if (newGroup == null || newGroup.isTransient()) {
            throw new ValidationException();
        }
        if (StringUtils.isEmpty(comments)) {
            throw new ValidationException();
        }
        newGroup = fetchService.fetch(newGroup);

        int changed = 0;
        int unchanged = 0;
        // force the result type to ITERATOR to avoid load all members in memory
        query.setIterateAll();
        final List<Member> members = (List<Member>) fullTextSearch(query);
        CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
        for (final Member member : members) {
            if (newGroup.equals(member.getGroup())) {
                unchanged++;
            } else {
                changeGroup(member, newGroup, comments);
                changed++;
            }

            cacheCleaner.clearCache();
        }
        return new BulkMemberActionResultVO(changed, unchanged);
    }

    @Override
    public <E extends Element> E changeGroup(E element, Group newGroup, final String comments)
            throws MemberHasBalanceException, MemberHasOpenInvoicesException, ValidationException {
        newGroup = fetchService.fetch(newGroup);

        // Validate the arguments
        final Element loggedElement = LoggedUser.element();
        if (element == null || newGroup == null || StringUtils.isEmpty(comments) || loggedElement.equals(element)) {
            throw new ValidationException();
        }

        // Check the current group
        element = load(element.getId(), Element.Relationships.USER, Element.Relationships.GROUP);
        final Group oldGroup = element.getGroup();
        if (oldGroup.equals(newGroup) || oldGroup.getStatus() == Group.Status.REMOVED) {
            throw new ValidationException();
        }

        if (element instanceof Member) {
            checkNewGroup((Member) element, (MemberGroup) newGroup);
        }

        if (newGroup.getStatus() == Group.Status.REMOVED) {
            // Disconnect the user if he is logged in
            try {
                accessService.disconnect(element.getUser());
            } catch (final NotConnectedException e) {
                // Ok - not logged in
            }

            if (element instanceof Member) {
                Member member = (Member) element;

                // Remove all ads
                final AdQuery adQuery = new AdQuery();
                adQuery.setOwner(member);
                adService.remove(EntityHelper.toIds(adService.search(adQuery)));

                // Remove all ad interests
                final AdInterestQuery adInterestQuery = new AdInterestQuery();
                adInterestQuery.setOwner(member);
                adInterestService.remove(EntityHelper.toIds(adInterestService.search(adInterestQuery)));

                // Remove all notification preferences
                notificationPreferenceDao.delete(EntityHelper.toIds(notificationPreferenceDao.load(member)));

                // Remove all contacts
                contactService.remove(EntityHelper.toIds(contactService.list(member)));

                // Cancel all cards
                cardService.cancelAllMemberCards(member);

                // Unassign all pos
                posService.unassignAllMemberPos(member);
            }
        }

        boolean noLongerBroker = false;
        if (element instanceof Member) {
            Member member = (Member) element;
            MemberGroup oldMemberGroup = (MemberGroup) oldGroup;
            MemberGroup newMemberGroup = (MemberGroup) newGroup;

            // Remove all brokerings if the member is no longer a broker
            noLongerBroker = oldMemberGroup.isBroker()
                    && (newGroup.getStatus() == Group.Status.REMOVED || !newMemberGroup.isBroker());
            if (noLongerBroker) {
                final BrokeringQuery brokeringQuery = new BrokeringQuery();
                brokeringQuery.setBroker(member);
                brokeringQuery.setResultType(ResultType.ITERATOR);
                final MessageSettings messageSettings = settingsService.getMessageSettings();
                String brokeringComments = messageSettings.getBrokerRemovedRemarkComments();
                brokeringComments = MessageProcessingHelper.processVariables(brokeringComments, member,
                        settingsService.getLocalSettings());
                for (final Brokering brokering : brokeringService.search(brokeringQuery)) {
                    brokeringService.remove(brokering, brokeringComments);
                }
            }

            // Update broker commissions
            if (oldMemberGroup.isBroker() || newMemberGroup.isBroker()) {
                commissionService.updateBrokerCommissions(member, oldMemberGroup, newMemberGroup);
            }

        }

        // Update the group
        element.setGroup(newGroup);
        elementDao.update(element);

        // Handle the member accounts
        if (element instanceof Member) {
            Member member = (Member) element;
            final boolean wasInactive = !member.isActive();
            handleAccounts(member);
            if (wasInactive && member.isActive()) {
                sendActivationMailIfNeeded(ActivationMail.ONLINE, member);
            }
        }

        // Creates the group remark and updates group history logs
        createGroupRemark(element, oldGroup, newGroup, comments);

        // Update the index
        elementDao.addToIndex(element);

        // Notify if was a broker and no longer is
        if (element instanceof Member && noLongerBroker) {
            Member member = (Member) element;
            memberNotificationHandler.removedFromBrokerGroupNotification(member);
        }

        return element;
    }

    @Override
    public Member changeMemberProfileByWebService(final ServiceClient client, final Member member) {
        if (member.isTransient()) {
            throw new UnexpectedEntityException();
        }
        final Element current = load(member.getId());
        final Set<MemberGroup> manageGroups = fetchService.fetch(client, ServiceClient.Relationships.MANAGE_GROUPS)
                .getManageGroups();
        if (!manageGroups.contains(current.getGroup())) {
            throw new PermissionDeniedException();
        }
        return save(member, ActivationMail.THREADED, WhenSaving.PROFILE, false);
    }

    @Override
    public <E extends Element> E changeProfile(final E element) {
        return save(element, null, WhenSaving.PROFILE, false);
    }

    @Override
    public PendingEmailChange confirmEmailChange(final String key) throws EntityNotFoundException {
        Member member = LoggedUser.member();
        PendingEmailChange pendingEmailChange = pendingEmailChangeDao.getByMember(member);
        if (pendingEmailChange == null) {
            throw new EntityNotFoundException(PendingEmailChange.class);
        }
        member.setEmail(pendingEmailChange.getNewEmail());
        elementDao.update(member);
        pendingEmailChangeDao.removeAll(member);
        return pendingEmailChange;
    }

    @Override
    public void createAgreementForAllMembers(final RegistrationAgreement registrationAgreement,
            final MemberGroup group) {
        elementDao.createAgreementForAllMembers(registrationAgreement, group);
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<? extends Element> fullTextSearch(final FullTextElementQuery query) {
        // Since the full text does not index group filters, only groups, disassemble the group filters into groups
        if (query instanceof FullTextMemberQuery) {
            final FullTextMemberQuery memberQuery = (FullTextMemberQuery) query;
            // groupFilters
            final Collection<GroupFilter> groupFilters = fetchService.fetch(memberQuery.getGroupFilters(),
                    GroupFilter.Relationships.GROUPS);
            if (CollectionUtils.isNotEmpty(groupFilters)) {
                final boolean hasGroupFilters = CollectionUtils.isNotEmpty(memberQuery.getGroupFilters());
                final boolean hasGroups = CollectionUtils.isNotEmpty(memberQuery.getGroups());
                final Set<Group> groupsFromFilters = new HashSet<Group>();
                if (hasGroupFilters) {
                    // Get all groups from selected group filters
                    for (final GroupFilter groupFilter : groupFilters) {
                        groupsFromFilters.addAll(groupFilter.getGroups());
                    }
                    if (hasGroups) {
                        // When there's both groups and group filters, use an intersection between them
                        memberQuery.setGroups(
                                CollectionUtils.intersection(groupsFromFilters, memberQuery.getGroups()));
                    } else {
                        // Else, use only the groups from group filters
                        memberQuery.setGroups(groupsFromFilters);
                    }
                }
                memberQuery.setGroupFilters(null);
            }
        }
        if (query.getNameDisplay() == null) {
            query.setNameDisplay(settingsService.getLocalSettings().getMemberResultDisplay());
        }
        query.setAnalyzer(settingsService.getLocalSettings().getLanguage().getAnalyzer());
        return elementDao.fullTextSearch(query);
    }

    @Override
    public ElementVO getElementVO(final long id) {
        return id > 0 ? elementDao.load(id).readOnlyView() : null;
    }

    @Override
    public Calendar getFirstMemberActivationDate() {
        return elementDao.getFirstMemberActivationDate();
    }

    @Override
    public PendingEmailChange getPendingEmailChange(final Member member) {
        return pendingEmailChangeDao.getByMember(member);
    }

    @Override
    public List<? extends Group> getPossibleNewGroups(final Element element) {
        Group group = fetchService.fetch(element, Element.Relationships.GROUP).getGroup();
        if (group.getStatus() == Group.Status.REMOVED) {
            return Collections.singletonList(group);
        }
        final GroupQuery query = new GroupQuery();
        if (group instanceof OperatorGroup) {
            query.setNatures(Group.Nature.OPERATOR);
            query.setMember(((OperatorGroup) group).getMember());
        } else {
            if (group.getNature() == Group.Nature.ADMIN) {
                query.setNatures(Group.Nature.ADMIN);
            } else {
                query.setNatures(Group.Nature.MEMBER, Group.Nature.BROKER);
            }
        }
        final List<? extends Group> groups = groupService.search(query);
        groups.remove(group);
        return groups;
    }

    @Override
    public Member insertMember(final Member member, final boolean ignoreActivationMail,
            final boolean validatePassword) {
        final ActivationMail activationMail = ignoreActivationMail ? ActivationMail.IGNORE
                : ActivationMail.THREADED;
        return save(member, activationMail, WhenSaving.IMPORT, false);
    }

    @Override
    public void invitePerson(final String email) {
        mailHandler.sendInvitation(LoggedUser.element(), email);
    }

    @Override
    public <T extends Element> T load(final Long id, final Relationship... fetch) {
        final T element = elementDao.<T>load(id, fetch);
        checkAccessToMember(element);
        return element;
    }

    @Override
    public Member loadByPrincipal(final PrincipalType principalType, String principal,
            final Relationship... fetch) {
        try {
            if (principal == null) {
                throw new NullPointerException();
            }
            final Principal principalEnum = principalType == null ? Principal.USER : principalType.getPrincipal();
            switch (principalEnum) {
            case USER:
                final User user = loadUser(principal);
                return (Member) fetchService.fetch(user.getElement(), fetch);
            case EMAIL:
                return (Member) elementDao.loadByEmail(principal, fetch);
            case CUSTOM_FIELD:
                final MemberCustomField customField = principalType.getCustomField();
                if (StringUtils.isNotEmpty(customField.getPattern())) {
                    principal = StringHelper.removeMask(customField.getPattern(), principal, false);
                }
                return elementDao.loadByCustomField(customField, principal, fetch);
            case CARD:
                // Use numbers only, avoid conflicts with formatting
                final String cardNumber = StringHelper.onlyNumbers(principal);
                final Card card = cardService.loadByNumber(new BigInteger(cardNumber), Card.Relationships.OWNER);
                final Calendar expirationDate = DateHelper.truncateNextDay(card.getExpirationDate());
                final boolean cardExpired = !Calendar.getInstance().getTime().before(expirationDate.getTime());
                if (card.getStatus() != Card.Status.ACTIVE || cardExpired) {
                    throw new EntityNotFoundException("The card " + cardNumber + " is not active or has expired.");
                }
                return fetchService.fetch(card.getOwner(), fetch);
            }
            throw new EntityNotFoundException(Member.class);
        } catch (final EntityNotFoundException e) {
            throw e;
        } catch (final Exception e) {
            final EntityNotFoundException enfe = new EntityNotFoundException(Member.class);
            enfe.initCause(e);
            throw enfe;
        }
    }

    @Override
    public OperatorUser loadOperatorUser(final Member member, final String operatorUsername,
            final Relationship... fetch) throws EntityNotFoundException {
        return userDao.loadOperator(member, operatorUsername, fetch);
    }

    @Override
    public PendingMember loadPendingMember(final Long id, final Relationship... fetch) {
        final PendingMember pendingMember = pendingMemberDao.load(id, fetch);
        checkManagement(pendingMember);
        return pendingMember;
    }

    @Override
    public PendingMember loadPendingMemberByKey(final String key, final Relationship... fetch) {
        return pendingMemberDao.loadByKey(key, fetch);
    }

    @Override
    public <T extends User> T loadUser(final Long id, final Relationship... fetch) {
        final T user = userDao.<T>load(id, fetch);
        checkAccessToMember(user.getElement());
        return user;
    }

    @Override
    public <T extends User> T loadUser(final String username, final Relationship... fetch) {
        final T user = userDao.<T>load(username, fetch);
        checkAccessToMember(user.getElement());
        return user;
    }

    @Override
    @SuppressWarnings("unchecked")
    public int processMembersExpirationForGroups(final Calendar time) {
        // Find on member groups...
        final GroupQuery query = new GroupQuery();
        query.setNatures(Group.Nature.MEMBER, Group.Nature.BROKER);
        int count = 0;
        final List<Group> groups = (List<Group>) groupService.search(query);
        final String message = messageResolver.message("changeGroup.member.expired");
        for (final Group group : groups) {
            final MemberGroup memberGroup = (MemberGroup) fetchService.fetch(group);
            final MemberGroupSettings memberSettings = memberGroup.getMemberSettings();
            final TimePeriod expireMembersAfter = memberSettings.getExpireMembersAfter();
            // ... those who expire members after a given time period ...
            if (expireMembersAfter == null || expireMembersAfter.getNumber() <= 0) {
                continue;
            }
            final Calendar limit = expireMembersAfter.remove(DateHelper.truncate(time));
            final List<Member> members = elementDao.listMembersRegisteredBeforeOnGroup(limit, memberGroup);
            final MemberGroup groupAfterExpiration = memberSettings.getGroupAfterExpiration();
            // ... then expire members on that group
            for (final Member member : members) {
                changeGroup(member, groupAfterExpiration, message);
                count++;
            }
        }
        return count;
    }

    @Override
    public Member publicValidateRegistration(final String key)
            throws EntityNotFoundException, RegistrationAgreementNotAcceptedException {
        final PendingMember pendingMember = pendingMemberDao.loadByKey(key, PendingMember.Relationships.values());

        // Store the agreement data
        final RegistrationAgreement registrationAgreement = pendingMember.getRegistrationAgreement();
        final Calendar agreementDate = pendingMember.getRegistrationAgreementDate();
        final String remoteAddress = pendingMember.getRemoteAddress();

        // Only proceed if the agreement (if any) has already been accepted
        final MemberGroup group = pendingMember.getMemberGroup();
        if (group.getRegistrationAgreement() != null
                && !group.getRegistrationAgreement().equals(registrationAgreement)) {
            throw new RegistrationAgreementNotAcceptedException();
        }

        // Eagerly delete to avoid the username being reported as already used
        pendingMemberDao.delete(pendingMember.getId());

        // Translate the PendingMember into a Member
        Member member = new Member();
        PropertyHelper.copyProperties(pendingMember, member, "id", "memberGroup", "username", "password",
                "customValues");
        member.setGroup(pendingMember.getMemberGroup());
        final MemberUser user = new MemberUser();
        member.setUser(user);
        user.setSalt(pendingMember.getSalt());
        user.setUsername(pendingMember.getUsername());
        final String password = pendingMember.getPassword();
        if (StringUtils.isNotEmpty(password)) {
            user.setPassword(password);
            if (!pendingMember.isForceChangePassword()) {
                user.setPasswordDate(Calendar.getInstance());
            }
        }

        // copy CF
        customFieldHelper.cloneFieldValues(pendingMember, member);

        member = save(member, ActivationMail.ONLINE, WhenSaving.EMAIL_VALIDATION,
                pendingMember.isForceChangePassword());

        // Mark the agreement as accepted
        if (registrationAgreement != null) {
            final RegistrationAgreementLog log = new RegistrationAgreementLog();
            log.setMember(member);
            log.setRegistrationAgreement(registrationAgreement);
            log.setDate(agreementDate);
            log.setRemoteAddress(remoteAddress);
            registrationAgreementLogDao.insert(log);
        }

        adminNotificationHandler.notifyNewPublicRegistration(member);

        return member;
    }

    @Override
    public void purgeOldEmailValidations(final Calendar time) {
        final LocalSettings localSettings = settingsService.getLocalSettings();
        final TimePeriod timePeriod = localSettings.getDeletePendingRegistrationsAfter();
        if (timePeriod == null || timePeriod.getNumber() <= 0) {
            return;
        }
        Calendar limit = timePeriod.remove(time);
        pendingMemberDao.deleteBefore(limit);
        pendingEmailChangeDao.deleteBefore(limit);
    }

    @Override
    public Object register(final Element element, final boolean forceChangePassword, final String remoteAddress) {
        if (element instanceof Administrator) {
            if (!forceChangePassword) {
                element.getUser().setPasswordDate(Calendar.getInstance());
            }
            return save(element, ActivationMail.ONLINE, WhenSaving.ADMIN_BY_ADMIN, forceChangePassword);
        } else if (element instanceof Member) {
            WhenSaving whenSaving;
            if (LoggedUser.getAccessType() == null) {
                whenSaving = WhenSaving.PUBLIC;
            } else if (LoggedUser.isBroker()) {
                whenSaving = WhenSaving.BY_BROKER;
            } else {
                whenSaving = WhenSaving.MEMBER_BY_ADMIN;
            }
            return register((Member) element, whenSaving, forceChangePassword, remoteAddress);
        } else if (element instanceof Operator) {
            Operator operator = (Operator) element;
            final Member loggedMember = LoggedUser.element();
            operator.setMember(loggedMember);
            operator.getUser().setPasswordDate(Calendar.getInstance());
            return save(operator, null, WhenSaving.OPERATOR, false);
        }
        // If not an admin, member or operator, what is it?
        throw new UnexpectedEntityException();
    }

    @Override
    public RegisteredMember registerMemberByWebService(ServiceClient client, final Member member,
            final String remoteAddress) {
        client = fetchService.fetch(client, ServiceClient.Relationships.MANAGE_GROUPS);
        final Set<MemberGroup> manageGroups = client.getManageGroups();
        if (manageGroups.isEmpty()) {
            throw new PermissionDeniedException();
        }
        MemberGroup group;
        try {
            group = (MemberGroup) fetchService.fetch(member.getGroup());
        } catch (final Exception e) {
            throw new EntityNotFoundException();
        }
        if (group == null) {
            group = manageGroups.iterator().next();
        }
        return register(member, WhenSaving.WEB_SERVICE, false, remoteAddress);
    }

    @Override
    public <T extends User> T reloadUser(final Long id, final Relationship... fetch) {
        final T user = userDao.<T>reload(id, fetch);
        checkAccessToMember(user.getElement());
        return user;
    }

    @Override
    public void remove(final Long id) throws UnexpectedEntityException {
        final Element element = load(id);
        if (element instanceof Member) {
            final Member member = (Member) element;
            if (member.getActivationDate() != null) {
                // Cannot permanently remove an active member
                throw new UnexpectedEntityException();
            }
        }
        elementDao.delete(id);
        elementDao.removeFromIndex(element);
    }

    @Override
    public int removePendingMembers(final Long... ids) {
        if (ids == null) {
            return 0;
        }
        for (final Long id : ids) {
            checkManagement(EntityHelper.reference(PendingMember.class, id));
        }
        return pendingMemberDao.delete(ids);
    }

    @Override
    public PendingMember resendEmail(final PendingMember pendingMember) throws MailSendingException {
        // Send the mail
        try {
            mailHandler.sendEmailValidation(pendingMember);
            pendingMember.setLastEmailDate(Calendar.getInstance());
            return pendingMemberDao.update(pendingMember);
        } finally {
            if (CurrentTransactionData.hasMailError()) {
                throw new MailSendingException("Email validation for " + pendingMember.getName());
            }
        }
    }

    @Override
    public PendingEmailChange resendEmailChange(final Long memberId) throws MailSendingException {
        Element element = load(memberId);
        if (!(element instanceof Member)) {
            throw new EntityNotFoundException(Member.class);
        }
        Member member = (Member) element;
        PendingEmailChange change = pendingEmailChangeDao.getByMember(member);
        return resendEmail(change);
    }

    @Override
    public List<? extends Element> search(final ElementQuery query) {
        query.fetch(Element.Relationships.USER);
        if (query.getOrder() == null) {
            query.setOrder(settingsService.getLocalSettings().getMemberResultDisplay());
        }
        return elementDao.search(query);
    }

    @Override
    public List<PendingMember> search(final PendingMemberQuery params) {
        Collection<MemberGroup> allowedGroups = null;
        if (LoggedUser.hasUser()) {
            if (LoggedUser.isBroker()) {
                final Member loggedBroker = LoggedUser.element();
                params.setBroker(loggedBroker);

                final BrokerGroup group = LoggedUser.group();
                allowedGroups = fetchService.fetch(group, BrokerGroup.Relationships.POSSIBLE_INITIAL_GROUPS)
                        .getPossibleInitialGroups();
            } else {
                final AdminGroup group = LoggedUser.group();
                allowedGroups = fetchService.fetch(group, AdminGroup.Relationships.MANAGES_GROUPS)
                        .getManagesGroups();
            }
        }
        if (allowedGroups != null) {
            if (allowedGroups.isEmpty()) {
                // No allowed group
                return Collections.emptyList();
            }
            // Ensure only the allowed groups are returned
            final Collection<MemberGroup> groups = params.getGroups();
            if (CollectionUtils.isEmpty(groups)) {
                params.setGroups(allowedGroups);
            } else {
                for (final Iterator<MemberGroup> iterator = groups.iterator(); iterator.hasNext();) {
                    final MemberGroup memberGroup = iterator.next();
                    if (!allowedGroups.contains(memberGroup)) {
                        iterator.remove();
                    }
                }
            }
        }
        return pendingMemberDao.search(params);
    }

    @Override
    public List<? extends Element> searchAtDate(final MemberQuery query, final Calendar date) {
        if (query.getOrder() == null) {
            query.setOrder(settingsService.getLocalSettings().getMemberResultDisplay());
        }
        return elementDao.searchAtDate(query, date);
    }

    public void setAccessServiceLocal(final AccessServiceLocal accessService) {
        this.accessService = accessService;
    }

    public void setAccountServiceLocal(final AccountServiceLocal accountService) {
        this.accountService = accountService;
    }

    public void setAdInterestServiceLocal(final AdInterestServiceLocal adInterestService) {
        this.adInterestService = adInterestService;
    }

    public void setAdminCustomFieldServiceLocal(final AdminCustomFieldServiceLocal adminCustomFieldService) {
        this.adminCustomFieldService = adminCustomFieldService;
    }

    public void setAdminNotificationHandler(final AdminNotificationHandler adminNotificationHandler) {
        this.adminNotificationHandler = adminNotificationHandler;
    }

    public void setAdServiceLocal(final AdServiceLocal adService) {
        this.adService = adService;
    }

    public void setBrokeringServiceLocal(final BrokeringServiceLocal brokeringService) {
        this.brokeringService = brokeringService;
    }

    public void setCardServiceLocal(final CardServiceLocal cardService) {
        this.cardService = cardService;
    }

    public void setChannelServiceLocal(final ChannelServiceLocal channelService) {
        this.channelService = channelService;
    }

    public void setCommissionServiceLocal(final CommissionServiceLocal commissionService) {
        this.commissionService = commissionService;
    }

    public void setContactServiceLocal(final ContactServiceLocal contactService) {
        this.contactService = contactService;
    }

    public void setCustomFieldHelper(final CustomFieldHelper customFieldHelper) {
        this.customFieldHelper = customFieldHelper;
    }

    public void setElementDao(final ElementDAO elementDAO) {
        elementDao = elementDAO;
    }

    public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
        this.fetchService = fetchService;
    }

    public void setGroupHistoryLogDao(final GroupHistoryLogDAO groupHistoryLogDao) {
        this.groupHistoryLogDao = groupHistoryLogDao;
    }

    public void setGroupServiceLocal(final GroupServiceLocal groupService) {
        this.groupService = groupService;
    }

    public void setHashHandler(final HashHandler hashHandler) {
        this.hashHandler = hashHandler;
    }

    public void setInvoiceServiceLocal(final InvoiceServiceLocal invoiceService) {
        this.invoiceService = invoiceService;
    }

    public void setLoanServiceLocal(final LoanServiceLocal loanService) {
        this.loanService = loanService;
    }

    public void setMailHandler(final MailHandler mailHandler) {
        this.mailHandler = mailHandler;
    }

    public void setMemberAccountHandler(final MemberAccountHandler memberAccountHandler) {
        this.memberAccountHandler = memberAccountHandler;
    }

    public void setMemberCustomFieldServiceLocal(final MemberCustomFieldServiceLocal memberCustomFieldService) {
        this.memberCustomFieldService = memberCustomFieldService;
    }

    public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
        this.memberNotificationHandler = memberNotificationHandler;
    }

    public void setMessageResolver(final MessageResolver messageResolver) {
        this.messageResolver = messageResolver;
    }

    public void setNotificationPreferenceDao(final NotificationPreferenceDAO notificationPreferenceDao) {
        this.notificationPreferenceDao = notificationPreferenceDao;
    }

    public void setOperatorCustomFieldServiceLocal(
            final OperatorCustomFieldServiceLocal operatorCustomFieldService) {
        this.operatorCustomFieldService = operatorCustomFieldService;
    }

    public void setPaymentServiceLocal(final PaymentServiceLocal paymentService) {
        this.paymentService = paymentService;
    }

    public void setPendingEmailChangeDao(final PendingEmailChangeDAO pendingEmailChangeDao) {
        this.pendingEmailChangeDao = pendingEmailChangeDao;
    }

    public void setPendingMemberDao(final PendingMemberDAO pendingMemberDao) {
        this.pendingMemberDao = pendingMemberDao;
    }

    public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
        this.permissionService = permissionService;
    }

    public void setPosServiceLocal(final PosServiceLocal posService) {
        this.posService = posService;
    }

    public void setPreferenceServiceLocal(final PreferenceServiceLocal preferenceService) {
        this.preferenceService = preferenceService;
    }

    @Override
    public void setRegistrationAgreementAgreed(PendingMember pendingMember) {
        pendingMember = fetchService.reload(pendingMember);
        RegistrationAgreement registrationAgreement = pendingMember.getMemberGroup().getRegistrationAgreement();
        if (registrationAgreement == null) {
            throw new ValidationException();
        }
        pendingMember.setRegistrationAgreement(registrationAgreement);
        pendingMember.setRegistrationAgreementDate(Calendar.getInstance());
        pendingMemberDao.update(pendingMember);
    }

    public void setRegistrationAgreementLogDao(final RegistrationAgreementLogDAO registrationAgreementLogDao) {
        this.registrationAgreementLogDao = registrationAgreementLogDao;
    }

    public void setRemarkServiceLocal(final RemarkServiceLocal remarkService) {
        this.remarkService = remarkService;
    }

    public void setScheduledPaymentServiceLocal(final ScheduledPaymentServiceLocal scheduledPaymentService) {
        this.scheduledPaymentService = scheduledPaymentService;
    }

    public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
        this.settingsService = settingsService;
    }

    public void setUniqueObjectHandler(final UniqueObjectHandler uniqueObjectHandler) {
        this.uniqueObjectHandler = uniqueObjectHandler;
    }

    public void setUserDao(final UserDAO userDAO) {
        userDao = userDAO;
    }

    public void setUsernameChangeLogDao(final UsernameChangeLogDAO usernameChangeLogDao) {
        this.usernameChangeLogDao = usernameChangeLogDao;
    }

    @Override
    public boolean shallAcceptAgreement(Member member) {
        member = fetchService.fetch(member, Element.Relationships.GROUP);
        final RegistrationAgreement registrationAgreement = member.getMemberGroup().getRegistrationAgreement();
        if (registrationAgreement == null) {
            return false;
        }
        final List<RegistrationAgreementLog> logs = registrationAgreementLogDao.listByMember(member);
        for (final RegistrationAgreementLog log : logs) {
            if (log.getRegistrationAgreement().equals(registrationAgreement)) {
                // Already accepted
                return false;
            }
        }
        // Not accepted yet
        return true;
    }

    @Override
    public PendingMember update(PendingMember pendingMember) {
        if (pendingMember == null || pendingMember.isTransient()) {
            throw new UnexpectedEntityException();
        }
        checkManagement(pendingMember);
        validate(pendingMember);
        final Collection<MemberCustomFieldValue> customValues = pendingMember.getCustomValues();
        pendingMember = pendingMemberDao.update(pendingMember);
        pendingMember.setCustomValues(customValues);
        memberCustomFieldService.saveValues(pendingMember);
        return pendingMember;
    }

    @Override
    public void validate(final Element element, final WhenSaving when, final boolean manualPassword)
            throws ValidationException {
        Group group = element.getGroup();
        // We need a group in order to validate
        if (group == null || group.isTransient()) {
            if (element.isTransient()) {
                // Cannot validate a new member without a group
                throw new ValidationException("group", "member.group", new RequiredError());
            } else {
                // If no new group is supplied, just keep the old group
                final Element loaded = load(element.getId(), Element.Relationships.GROUP);
                group = loaded.getGroup();
                // Put the old group back on the element
                element.setGroup(group);
            }
        } else {
            group = fetchService.fetch(group);
        }
        if (element instanceof Member && element.isPersistent()) {
            // We must fill in the custom values by their default values if the member cannot edit it, so validation won't fail
            final Member member = (Member) element;
            final Collection<MemberCustomFieldValue> customValues = member.getCustomValues();
            List<MemberCustomField> fields = memberCustomFieldService.list();
            fields = customFieldHelper.onlyForGroup(fields, (MemberGroup) group);
            final Member current = (Member) load(element.getId(), Member.Relationships.CUSTOM_VALUES);
            final Collection<MemberCustomFieldValue> currentValues = current.getCustomValues();
            final Element loggedElement = LoggedUser.hasUser() ? LoggedUser.element() : null;
            final boolean byOwner = loggedElement != null && loggedElement.equals(current);
            boolean byBroker = false;
            if (loggedElement != null && LoggedUser.isBroker()) {
                byBroker = loggedElement.equals(current.getBroker());
            }
            final Group loggedGroup = LoggedUser.hasUser() ? LoggedUser.group() : null;
            for (final MemberCustomField field : fields) {
                if (loggedGroup != null && !field.getUpdateAccess().granted(loggedGroup, byOwner, byBroker, false,
                        when == WhenSaving.WEB_SERVICE)) {
                    final MemberCustomFieldValue currentValue = customFieldHelper.findByField(field, currentValues);
                    final MemberCustomFieldValue value = customFieldHelper.findByField(field, customValues);
                    if (value != null) {
                        customValues.remove(value);
                    }
                    if (currentValue != null) {
                        customValues.add(currentValue);
                    }
                }
            }
        }
        createValidator(group, element, when, manualPassword).validate(element);
    }

    @Override
    public void validate(final PendingMember pendingMember) throws ValidationException {
        getValidator(pendingMember).validate(pendingMember);
    }

    @Override
    @SuppressWarnings("unchecked")
    public void validateBulkChangeChannels(final FullTextMemberQuery query,
            final Collection<Channel> enableChannels, final Collection<Channel> disableChannels) {
        Collection<Channel> intersection = CollectionUtils.intersection(enableChannels, disableChannels);
        if (CollectionUtils.isNotEmpty(intersection)) {
            throw new ValidationException("changeChannels.invalidChannelsSelection", toString(intersection));
        }
    }

    private void cancelScheduledPaymentsAndNotify(final Member member, final MemberGroup newGroup) {
        Collection<MemberAccountType> accountTypes = newGroup.getAccountTypes();
        if (accountTypes == null) {
            accountTypes = Collections.emptyList();
        }

        scheduledPaymentService.cancelScheduledPaymentsAndNotify(member, accountTypes);
    }

    private void checkAccessToMember(final Element element) {
        if (element instanceof Member && LoggedUser.hasUser()) {
            final Member member = fetchService.fetch((Member) element, Member.Relationships.BROKER);
            if ((LoggedUser.isMember() || LoggedUser.isOperator())) {
                final Member loggedMember = (Member) LoggedUser.accountOwner();
                if (!loggedMember.equals(member)) {
                    final MemberGroup group = fetchService.fetch(loggedMember.getMemberGroup(),
                            MemberGroup.Relationships.CAN_VIEW_PROFILE_OF_GROUPS);
                    final Collection<MemberGroup> canViewMembersOfGroups = group.getCanViewProfileOfGroups();
                    if (!canViewMembersOfGroups.contains(element.getGroup())
                            && !loggedMember.equals(member.getBroker())) {
                        throw new PermissionDeniedException();
                    }
                }
            } else if (LoggedUser.isAdministrator()) {
                final AdminGroup group = LoggedUser.group();
                final Collection<MemberGroup> managesGroups = fetchService
                        .fetch(group, AdminGroup.Relationships.MANAGES_GROUPS).getManagesGroups();
                if (!managesGroups.contains(member.getGroup())) {
                    throw new PermissionDeniedException();
                }
            }
        }
    }

    private void checkManagement(PendingMember pendingMember) {
        boolean valid = false;
        if (LoggedUser.hasUser()) {
            pendingMember = fetchService.fetch(pendingMember);
            if (LoggedUser.isBroker()) {
                final Member loggedBroker = LoggedUser.element();
                valid = loggedBroker.equals(pendingMember.getBroker());
            } else {
                final AdminGroup group = LoggedUser.group();
                final Collection<MemberGroup> managesGroups = fetchService
                        .reload(group, AdminGroup.Relationships.MANAGES_GROUPS).getManagesGroups();
                valid = managesGroups.contains(pendingMember.getMemberGroup());
            }
        }
        if (!valid) {
            throw new PermissionDeniedException();
        }
    }

    private void checkNewGroup(final Member member, final MemberGroup newGroup) {
        Collection<MemberAccountType> accountTypes = newGroup.getAccountTypes();
        if (accountTypes == null) {
            accountTypes = Collections.emptyList();
        }

        // Check if the member has any open loans
        final LoanQuery lQuery = new LoanQuery();
        lQuery.fetch(RelationshipHelper.nested(Loan.Relationships.TRANSFER, Payment.Relationships.TYPE));
        lQuery.setStatus(Loan.Status.OPEN);
        lQuery.setMember(member);
        AccountType accType;
        for (final Loan loan : loanService.search(lQuery)) {
            final LoanParameters params = loan.getTransferType().getLoan();
            if (!accountTypes.contains(getFrom(params.getRepaymentType()))) {
                throw new MemberHasPendingLoansException(newGroup);
            }

            if (params.getType() == Loan.Type.WITH_INTEREST) {
                if ((accType = getFrom(params.getMonthlyInterestRepaymentType())) != null
                        && !accountTypes.contains(accType)) {
                    throw new MemberHasPendingLoansException(newGroup);
                }
                if ((accType = getFrom(params.getGrantFeeRepaymentType())) != null
                        && !accountTypes.contains(accType)) {
                    throw new MemberHasPendingLoansException(newGroup);
                }
                if ((accType = getFrom(params.getExpirationFeeRepaymentType())) != null
                        && !accountTypes.contains(accType)) {
                    throw new MemberHasPendingLoansException(newGroup);
                }
                if ((accType = getFrom(params.getExpirationDailyInterestRepaymentType())) != null
                        && !accountTypes.contains(accType)) {
                    throw new MemberHasPendingLoansException(newGroup);
                }
            }
        }

        // Check if the member has any open invoices
        final InvoiceQuery invoiceQuery = new InvoiceQuery();
        invoiceQuery.setDirection(InvoiceQuery.Direction.INCOMING);
        invoiceQuery.setOwner(member);
        invoiceQuery.setStatus(Invoice.Status.OPEN);
        for (final Invoice invoice : invoiceService.search(invoiceQuery)) {
            boolean found = false;
            final Iterator<MemberAccountType> accIt = accountTypes.iterator();
            while (!found && accIt.hasNext()) {
                accType = accIt.next();
                final Iterator<TransferType> ttIt = accType.getFromTransferTypes().iterator();
                while (!found && ttIt.hasNext()) {
                    final TransferType tt = ttIt.next();
                    if (tt.getTo().equals(invoice.getDestinationAccountType())) {
                        found = true;
                    }
                }
            }
            if (!found) {
                throw new MemberHasOpenInvoicesException(newGroup);
            }
        }

        invoiceQuery.setDirection(InvoiceQuery.Direction.OUTGOING);
        for (final Invoice invoice : invoiceService.search(invoiceQuery)) {
            if (!accountTypes.contains(invoice.getDestinationAccountType())) {
                throw new MemberHasOpenInvoicesException(newGroup);
            }
        }

        // Cancel all incoming and outgoing scheduled payments and notify payers/receivers
        // We cancel the payments here and not in the changeGroup method because we must check the balance after the cancellation
        // to ensure we take in to account the reserved amount (if any) that will be returned to the from account.
        cancelScheduledPaymentsAndNotify(member, newGroup);

        // Check the account balance
        final BigDecimal minimumPayment = paymentService.getMinimumPayment();
        for (final Account account : accountService.getAccounts(member)) {
            final BigDecimal balance = accountService.getBalance(new AccountDateDTO(account));
            if (!accountTypes.contains(account.getType()) && (balance.abs().compareTo(minimumPayment) > 0)) {
                throw new MemberHasBalanceException(newGroup, (MemberAccountType) account.getType());
            }
        }
    }

    private RegistrationAgreementLog createAgreementLog(final String remoteAddress, final Member member,
            final RegistrationAgreement registrationAgreement) {
        final RegistrationAgreementLog log = new RegistrationAgreementLog();
        log.setMember(member);
        log.setRegistrationAgreement(registrationAgreement);
        log.setDate(Calendar.getInstance());
        log.setRemoteAddress(remoteAddress);
        return registrationAgreementLogDao.insert(log);
    }

    private void createGroupHistoryLog(final Element element, final Group group, final Calendar start) {
        final GroupHistoryLog newGhl = new GroupHistoryLog();
        newGhl.setElement(element);
        newGhl.setGroup(group);
        newGhl.setPeriod(Period.begginingAt(start));
        groupHistoryLogDao.insert(newGhl);
    }

    /**
     * Create a remark for a group change
     */
    private void createGroupRemark(final Element member, final Group oldGroup, final Group newGroup,
            final String comments) {
        final Calendar now = Calendar.getInstance();
        final GroupRemark remark = new GroupRemark();
        if (LoggedUser.hasUser()) {
            remark.setWriter(LoggedUser.element());
        }
        remark.setSubject(member);
        remark.setDate(now);
        remark.setOldGroup(oldGroup);
        remark.setNewGroup(newGroup);
        remark.setComments(comments);
        remarkService.save(remark);

        updateGroupHistoryLogs(member, newGroup, now);
    }

    /**
     * Creates a validator for the given group
     */
    private Validator createValidator(final Group group, final Element element, final WhenSaving when,
            final boolean manualPassword) {
        final Element.Nature nature = group.getNature().getElementNature();
        final String baseName = nature.name().toLowerCase();
        return new DelegatingValidator(new DelegatingValidator.DelegateSource() {
            @Override
            public Validator getValidator() {
                final AccessSettings accessSettings = settingsService.getAccessSettings();
                final LocalSettings localSettings = settingsService.getLocalSettings();
                final Validator validator = new Validator(baseName);
                validator.property("group").required();
                validator.property("name").required().maxLength(100);
                final boolean isMember = nature == Element.Nature.MEMBER;

                ServiceClient client = LoggedUser.serviceClient();

                // Validate username
                if ((element.isTransient()
                        && (!isMember || accessSettings.getUsernameGeneration() == UsernameGeneration.NONE))
                        || element.isPersistent()) {
                    final Property username = validator.property("username");
                    username.required();

                    // Checks that the username is not yet used
                    validator.general(new ExistingUsernameValidation());

                    final RangeConstraint usernameLength = accessSettings.getUsernameLength();
                    if (usernameLength != null) {
                        username.add(new LengthValidation(usernameLength)).regex(accessSettings.getUsernameRegex());
                    }
                }
                if (element.isTransient()) {
                    // When manual password or public registration, login password is always required
                    boolean loginPasswordRequired = manualPassword || when == WhenSaving.PUBLIC;
                    if (client != null) {
                        // For service clients, if the channel uses the login password, it is required as well
                        final Channel channel = client.getChannel();
                        if (channel.getCredentials() == Credentials.DEFAULT
                                || channel.getCredentials() == Credentials.LOGIN_PASSWORD) {
                            loginPasswordRequired = true;
                        }
                    }
                    // Validate the password on insert
                    final Property password = validator.property("user.password").key("createMember.password");
                    if (loginPasswordRequired) {
                        password.required();
                    }
                    // We can only validate the password if it's not pre-hashed
                    if (!when.isPreHashed()) {
                        accessService.addLoginPasswordValidation(element, password);
                    }

                    // Validate the pin, if any
                    if (isMember) {
                        boolean pinRequired = false;
                        if (client != null) {
                            // For service clients, if the channel uses the login password, it is required as well
                            final Channel channel = client.getChannel();
                            if (channel.getCredentials() == Credentials.PIN) {
                                pinRequired = true;
                            }
                        }
                        final Property pin = validator.property("user.pin").key("channel.credentials.PIN");
                        if (pinRequired) {
                            pin.required();
                        }
                        if (!when.isPreHashed()) {
                            accessService.addPinValidation((Member) element, pin);
                        }
                    }
                }

                // Validate the email
                final Property email = validator.property("email");
                // Email is not required for operators nor service clients which are set to ignore validation
                final boolean ignoreValidation = nature == Element.Nature.OPERATOR
                        || client != null && client.isIgnoreRegistrationValidations();
                if (!ignoreValidation && localSettings.isEmailRequired()) {
                    email.required();
                }
                email.add(EmailValidation.instance()).maxLength(100);
                if (localSettings.isEmailUnique()) {
                    email.add(new UniqueEmailValidation(element.getId(), when == WhenSaving.PUBLIC));
                }

                // Custom fields
                validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
                    @Override
                    public Validator getValidator() {
                        switch (nature) {
                        case ADMIN:
                            return adminCustomFieldService.getValueValidator((AdminGroup) group);
                        case MEMBER:
                            Member member = (Member) element;
                            MemberCustomField.Access access = null;
                            if (!LoggedUser.hasUser()) {
                                access = MemberCustomField.Access.REGISTRATION;
                            } else {
                                member = fetchService.fetch(member, Member.Relationships.BROKER);
                                final Element loggedElement = LoggedUser.element();
                                if (loggedElement.equals(element)) {
                                    access = MemberCustomField.Access.MEMBER;
                                } else if ((member == null && LoggedUser.isBroker())
                                        || (member != null && loggedElement.equals(member.getBroker()))) {
                                    access = MemberCustomField.Access.BROKER;
                                } else if (loggedElement instanceof Administrator) {
                                    access = MemberCustomField.Access.ADMIN;
                                }
                            }
                            return memberCustomFieldService.getValueValidator((MemberGroup) group, access);
                        case OPERATOR:
                            return operatorCustomFieldService
                                    .getValueValidator(((OperatorGroup) group).getMember());
                        }
                        return null;
                    }
                }));
                return validator;
            }
        });
    }

    /**
     * Generate a member username
     */
    private String generateUsername(final int length) {
        String generated;
        boolean exists;
        do {
            // Generate a random number
            generated = RandomStringUtils.randomNumeric(length);
            if (generated.charAt(0) == '0') {
                // The first character cannot be zero
                generated = (new Random().nextInt(8) + 1) + generated.substring(1);
            }
            // Check if such username exists
            try {
                userDao.load(generated);
                exists = true;
            } catch (final EntityNotFoundException e) {
                exists = false;
            }
        } while (exists);
        return generated;
    }

    private nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation getEmailValidation(
            final WhenSaving whenSaving, final Element element) {
        if (whenSaving == null || element.getNature() != Element.Nature.MEMBER) {
            return null;
        }
        switch (whenSaving) {
        case BY_BROKER:
            return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.BROKER;
        case MEMBER_BY_ADMIN:
            return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.ADMIN;
        case PROFILE:
            if (LoggedUser.serviceClient() != null) {
                return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.WEB_SERVICE;
            } else if (LoggedUser.element().equals(element)) {
                return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.USER;
            } else if (LoggedUser.isBroker()) {
                return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.BROKER;
            } else if (LoggedUser.isAdministrator()) {
                return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.ADMIN;
            }
        case PUBLIC:
            return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.USER;
        case WEB_SERVICE:
            return nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation.WEB_SERVICE;
        }
        return null;
    }

    private AccountType getFrom(final TransferType tt) {
        return tt == null ? null : tt.getFrom();
    }

    private Validator getValidator(final PendingMember pendingMember) {
        final AccessSettings accessSettings = settingsService.getAccessSettings();
        final MemberGroup group = pendingMember.getMemberGroup();
        final Validator validator = new Validator("member");
        validator.property("name").required().maxLength(100);
        if (accessSettings.getUsernameGeneration() == UsernameGeneration.NONE) {
            validator.property("username").required().maxLength(64);
        }
        validator.property("email").required().maxLength(100).add(new PendingMemberEmailValidation(pendingMember));

        if (group != null) {
            validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
                @Override
                public Validator getValidator() {
                    MemberCustomField.Access access = null;
                    if (!LoggedUser.hasUser()) {
                        access = MemberCustomField.Access.REGISTRATION;
                    } else {
                        if (LoggedUser.element().equals(pendingMember.getBroker())) {
                            access = MemberCustomField.Access.BROKER;
                        } else {
                            access = MemberCustomField.Access.ADMIN;
                        }
                    }
                    return memberCustomFieldService.getValueValidator(group, access);
                }
            }));
        }
        validator.general(new ExistingUsernameValidation());
        return validator;
    }

    /**
     * This method creates the accounts related to the member group, and marks those not related as inactive
     */
    @SuppressWarnings("unchecked")
    private void handleAccounts(Member member) {
        member = fetchService.fetch(member,
                RelationshipHelper.nested(Element.Relationships.GROUP, MemberGroup.Relationships.ACCOUNT_SETTINGS));
        final MemberGroup group = member.getMemberGroup();
        final Collection<MemberGroupAccountSettings> accountSettings = group.getAccountSettings();
        final List<MemberAccount> accounts = (List<MemberAccount>) accountService.getAccounts(member,
                Account.Relationships.TYPE);
        // Mark as inactive the accounts no longer used
        for (final MemberAccount account : accounts) {
            if (!hasAccount(account, accountSettings)) {
                memberAccountHandler.deactivate(account, group.getStatus() == Group.Status.REMOVED);
            }
        }
        // Create the accounts the member does not yet has
        if (!CollectionUtils.isEmpty(accountSettings)) {
            for (final MemberGroupAccountSettings settings : accountSettings) {
                memberAccountHandler.activate(member, settings.getAccountType());
            }
        }
        // Activate members without accounts but in active groups
        if (member.getActivationDate() == null && group.isActive()) {
            member.setActivationDate(Calendar.getInstance());
        }
    }

    /**
     * Check if the specified account belongs to any of the accountSettings
     */
    private boolean hasAccount(final MemberAccount account,
            final Collection<MemberGroupAccountSettings> accountSettings) {
        for (final MemberGroupAccountSettings settings : accountSettings) {
            if (account.getType().equals(settings.getAccountType())) {
                return true;
            }
        }
        return false;
    }

    private RegisteredMember register(Member member, final WhenSaving when, final boolean forceChangePassword,
            final String remoteAddress) {
        final MemberGroup group = (MemberGroup) fetchService.fetch(member.getGroup());
        member.setGroup(group);

        RegisteredMember result;

        // Check the mail validation
        final MemberGroupSettings settings = group.getMemberSettings();
        nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation emailValidation = getEmailValidation(
                when, member);
        final boolean validateEmail = settings.getEmailValidation() != null
                && settings.getEmailValidation().contains(emailValidation);
        if (validateEmail) {
            // It's enabled: Save a pending member
            final PendingMember pendingMember = new PendingMember();
            PropertyHelper.copyProperties(member, pendingMember);
            pendingMember.setCreationDate(Calendar.getInstance());
            pendingMember.setSalt(hashHandler.newSalt());
            pendingMember.setForceChangePassword(forceChangePassword);
            final User user = member.getUser();
            if (user != null) {
                pendingMember.setPassword(hashHandler.hash(pendingMember.getSalt(), user.getPassword()));
            }
            if (user instanceof MemberUser) {
                final MemberUser memberUser = (MemberUser) user;
                pendingMember.setPin(hashHandler.hash(pendingMember.getSalt(), memberUser.getPin()));
            }
            pendingMember.setValidationKey(RandomStringUtils.randomAlphanumeric(64));
            if (when == WhenSaving.PUBLIC) {
                // On public registrations, the license agreement has been accepted
                final RegistrationAgreement registrationAgreement = group.getRegistrationAgreement();
                if (registrationAgreement != null) {
                    pendingMember.setRegistrationAgreement(registrationAgreement);
                    pendingMember.setRegistrationAgreementDate(Calendar.getInstance());
                }
            }
            validate(pendingMember);
            result = pendingMemberDao.insert(pendingMember);

            memberCustomFieldService.saveValues(result);

            resendEmail((PendingMember) result);
        } else {
            // Not enabled: save the member directly
            final ActivationMail activationMail = when == WhenSaving.WEB_SERVICE ? ActivationMail.THREADED
                    : ActivationMail.ONLINE;
            result = member = save(member, activationMail, when, forceChangePassword);

            if (when == WhenSaving.PUBLIC) {
                // On the public registration, when there's an agreement, store it
                member = fetchService.fetch(member, RelationshipHelper.nested(Element.Relationships.GROUP,
                        MemberGroup.Relationships.REGISTRATION_AGREEMENT));
                final RegistrationAgreement registrationAgreement = member.getMemberGroup()
                        .getRegistrationAgreement();
                if (registrationAgreement != null) {
                    createAgreementLog(remoteAddress, member, registrationAgreement);
                }
            }

            // Notify the admins
            adminNotificationHandler.notifyNewPublicRegistration(member);
        }
        return result;
    }

    private PendingEmailChange resendEmail(final PendingEmailChange pendingEmailChange)
            throws MailSendingException {
        // Send the mail
        try {
            mailHandler.sendEmailChange(pendingEmailChange);
            pendingEmailChange.setLastEmailDate(Calendar.getInstance());
            return pendingEmailChangeDao.update(pendingEmailChange);
        } finally {
            if (CurrentTransactionData.hasMailError()) {
                throw new MailSendingException(
                        "Email change validation for " + pendingEmailChange.getMember().getName());
            }
        }
    }

    /**
     * Saves the given element
     */
    @SuppressWarnings("unchecked")
    private <E extends Element> E save(E element, final ActivationMail activationMail, final WhenSaving when,
            final boolean forceChangePassword) {
        validate(element, when, false);
        // Store the custom values on a saparate collection
        final Collection<?> values = (Collection<?>) PropertyHelper.get(element, "customValues");
        PropertyHelper.set(element, "customValues", null);

        final boolean isInsert = element.isTransient();

        if (isInsert) {
            // Check if we must generate a username for member
            final AccessSettings accessSettings = settingsService.getAccessSettings();
            final UsernameGeneration usernameGeneration = accessSettings.getUsernameGeneration();
            if (element instanceof Member && usernameGeneration != UsernameGeneration.NONE) {
                User user = element.getUser();
                // Assign a new user if none found
                if (user == null) {
                    user = new MemberUser();
                    element.setUser(user);
                }
                // Generate the username
                String generated = generateUsername(accessSettings.getGeneratedUsernameLength());
                while (!uniqueObjectHandler.tryAcquire(Pair.<Object, Object>of(generated, generated))) {
                    generated = generateUsername(accessSettings.getGeneratedUsernameLength());
                }
                user.setUsername(generated);
            } else {
                // Check if the username already in use
                final String username = element.getUsername();
                if (!uniqueObjectHandler.tryAcquire(Pair.<Object, Object>of(username, username))) {
                    throw new UsernameAlreadyInUseException(username);
                }
                try {
                    if (element instanceof Operator) {
                        loadOperatorUser((Member) LoggedUser.element(), username);
                    } else {
                        loadUser(username);
                    }
                    throw new UsernameAlreadyInUseException(username);
                } catch (final EntityNotFoundException e) {
                    // Ok - not exists yet
                }
            }

            final String email = StringUtils.trimToNull(element.getEmail());
            if (settingsService.getLocalSettings().isEmailUnique() && StringUtils.isNotEmpty(email)) {
                if (!uniqueObjectHandler.tryAcquire(Pair.<Object, Object>of(email, email))) {
                    throw new ValidationException(new UniqueError(email));
                }
            }

            final User user = element.getUser();
            // Create a salt value
            if (user.getSalt() == null) {
                user.setSalt(hashHandler.newSalt());
            }
            // If a password exists, ensure it's hashed
            if (StringUtils.isNotEmpty(user.getPassword())) {
                // When the registration is not pre-hashed, hash the password
                if (!when.isPreHashed()) {
                    user.setPassword(hashHandler.hash(user.getSalt(), user.getPassword()));
                }
                // When not forcing to change (passwordDate == null), set a password date
                if (!forceChangePassword) {
                    user.setPasswordDate(Calendar.getInstance());
                }
            }
            // If a pin exists, ensure it's hashed
            if (user instanceof MemberUser) {
                final MemberUser memberUser = (MemberUser) user;
                if (StringUtils.isNotEmpty(memberUser.getPin()) && !when.isPreHashed()) {
                    memberUser.setPin(hashHandler.hash(user.getSalt(), memberUser.getPin()));
                }
            }

            // Insert
            Calendar creationDate = element.getCreationDate();
            if (creationDate == null) {
                creationDate = Calendar.getInstance();
                element.setCreationDate(creationDate);
            }
            element = elementDao.insert(element);
            if (element instanceof Member) {
                final Member member = (Member) element;

                // Handle the member accounts
                handleAccounts(member);

                // When the member has been activated, send the activation e-mail
                if (member.isActive()) {
                    sendActivationMailIfNeeded(activationMail, member);
                }

                // Fetch member group
                final MemberGroup group = fetchService.fetch(member.getMemberGroup(),
                        MemberGroup.Relationships.CHANNELS, MemberGroup.Relationships.DEFAULT_MAIL_MESSAGES,
                        MemberGroup.Relationships.SMS_MESSAGES, MemberGroup.Relationships.DEFAULT_SMS_MESSAGES);

                // Copy default channels access from to member
                final Collection<Channel> memberChannels = new ArrayList<Channel>(group.getDefaultChannels());
                member.setChannels(memberChannels);
                element = (E) elementDao.update(member, false);

                // Insert the default notification preferences
                final List<NotificationPreference> preferences = new ArrayList<NotificationPreference>();
                final Collection<Type> defaultMailMessages = group.getDefaultMailMessages();
                final Collection<Type> smsMessages = group.getSmsMessages();
                final Collection<Type> defaultSmsMessages = group.getDefaultSmsMessages();
                for (final Type type : Type.values()) {
                    final NotificationPreference preference = new NotificationPreference();
                    preference.setEmail(defaultMailMessages.contains(type));
                    preference.setMessage(true);
                    if (smsMessages.contains(type) && defaultSmsMessages.contains(type)) {
                        preference.setSms(true);
                    }
                    preference.setMember(member);
                    preference.setType(type);
                    preferences.add(preference);
                }
                preferenceService.save(member, preferences);

                // Create the brokering when there is a broker set
                final Member broker = member.getBroker();
                if (broker != null) {
                    brokeringService.create(broker, member);
                }
            }

            // Create initial group history log
            createGroupHistoryLog(element, element.getGroup(), creationDate);
        } else {
            // Some properties cannot be saved using this method. Load the db state
            final Element saved = elementDao.load(element.getId());
            element.setCreationDate(saved.getCreationDate());
            element.setGroup(saved.getGroup());
            final User user = saved.getUser();
            if (element instanceof Member) {

                /*
                 * At this point if there is not a valid user, the update was invoked through an unrestricted web service client.
                 */
                final boolean isWebServiceInvocation = !LoggedUser.hasUser();

                final Member member = (Member) element;

                // Check if the name has changed
                final String savedName = saved.getName();
                final String givenName = element.getName();
                if (!savedName.equals(givenName)) {
                    final boolean canChangeName = isWebServiceInvocation
                            || permissionService.permission(member).admin(AdminMemberPermission.MEMBERS_CHANGE_NAME)
                                    .broker(BrokerPermission.MEMBERS_CHANGE_NAME)
                                    .member(MemberPermission.PROFILE_CHANGE_NAME).hasPermission();
                    if (!canChangeName) {
                        // No permissions. Ensure the name is not changed
                        member.setName(savedName);
                    }
                }

                // Check if the email has changed
                final String savedEmail = StringUtils.trimToNull(saved.getEmail());
                final String givenEmail = StringUtils.trimToNull(element.getEmail());
                if (!ObjectUtils.equals(savedEmail, givenEmail)) {
                    final boolean canChangeEmail = isWebServiceInvocation || permissionService.permission(member)
                            .admin(AdminMemberPermission.MEMBERS_CHANGE_EMAIL)
                            .broker(BrokerPermission.MEMBERS_CHANGE_EMAIL)
                            .member(MemberPermission.PROFILE_CHANGE_EMAIL).hasPermission();
                    if (!canChangeEmail) {
                        // No permissions. Ensure the email is not changed
                        member.setEmail(savedEmail);
                    } else {
                        pendingEmailChangeDao.removeAll(member);
                        // Check if there is e-mail validation for changing e-mail
                        if (givenEmail != null) {
                            nl.strohalm.cyclos.entities.groups.MemberGroupSettings.EmailValidation emailValidation = getEmailValidation(
                                    when, element);
                            if (member.getMemberGroup().getMemberSettings().getEmailValidation()
                                    .contains(emailValidation)) {
                                // E-mail validation is enabled. Keep the same saved e-mail and create a pending e-mail change
                                member.setEmail(savedEmail);
                                PendingEmailChange pec = new PendingEmailChange();
                                pec.setBy(LoggedUser.element());
                                pec.setCreationDate(Calendar.getInstance());
                                pec.setMember(member);
                                pec.setNewEmail(givenEmail);
                                pec.setRemoteAddress(LoggedUser.remoteAddress());
                                pec.setValidationKey(RandomStringUtils.randomAlphanumeric(64));
                                pec = pendingEmailChangeDao.insert(pec);
                                resendEmail(pec);
                            }
                        }
                    }
                }

                // Check if the username has changed
                final String savedUsername = saved.getUsername();
                final String givenUsername = element.getUsername();
                boolean canChangeUsername;
                if (settingsService.getAccessSettings().getUsernameGeneration() == UsernameGeneration.NONE) {
                    canChangeUsername = isWebServiceInvocation || permissionService.permission(member)
                            .admin(AdminMemberPermission.MEMBERS_CHANGE_USERNAME)
                            .broker(BrokerPermission.MEMBERS_CHANGE_USERNAME)
                            .member(MemberPermission.PROFILE_CHANGE_USERNAME).hasPermission();
                } else {
                    // Even with permissions, when username is generated it cannot be changed
                    canChangeUsername = false;
                }
                if (!savedUsername.equals(givenUsername) && canChangeUsername) {
                    // Log the change
                    final UsernameChangeLog log = new UsernameChangeLog();
                    log.setDate(Calendar.getInstance());
                    log.setBy(LoggedUser.element());
                    log.setUser(user);
                    log.setPreviousUsername(savedUsername);
                    log.setNewUsername(givenUsername);
                    usernameChangeLogDao.insert(log);

                    // Save the username
                    user.setUsername(givenUsername);

                    // Set the owner name on each account
                    final List<? extends Account> accounts = accountService.getAccounts(member);
                    for (final Account account : accounts) {
                        account.setOwnerName(givenUsername);
                    }
                }
            } else if (element instanceof Operator) {
                if (LoggedUser.isMember()) {
                    // Save the username: a member always can change the operator's username
                    user.setUsername(element.getUsername());
                }
            }
            element.setUser(user);
            if (element instanceof Member) {
                final Member member = (Member) element;
                final Member savedMember = (Member) saved;
                member.setActivationDate(savedMember.getActivationDate());
                member.setBroker(savedMember.getBroker());
                member.setChannels(accessService.getChannelsEnabledForMember(savedMember));
            } else if (element instanceof Operator) {
                final Operator operator = (Operator) element;
                final Operator savedOperator = (Operator) saved;
                operator.setMember(savedOperator.getMember());
                if (!LoggedUser.isMember()) { // preserve the saved name: only the member can change the operator's name
                    operator.setName(savedOperator.getName());
                }
            }

            // Update
            element = elementDao.update(element);
        }

        // Save the custom fields
        PropertyHelper.set(element, "customValues", values);
        if (element instanceof Member) {
            memberCustomFieldService.saveValues((Member) element);
        } else if (element instanceof Administrator) {
            adminCustomFieldService.saveValues((Administrator) element);
        } else if (element instanceof Operator) {
            operatorCustomFieldService.saveValues((Operator) element);
        }

        // Reindex the element
        elementDao.addToIndex(element);

        return element;
    }

    private void sendActivationMailIfNeeded(final ActivationMail activationMail, final Member member) {
        if (activationMail == ActivationMail.IGNORE || StringUtils.isEmpty(member.getEmail())) {
            return;
        }
        final MemberGroup group = member.getMemberGroup();
        final User user = member.getUser();
        // Check if the member is activated, and activation mail can be sent
        final boolean sendPasswordByEmail = group.getMemberSettings().isSendPasswordByEmail();
        String password = null;
        if (sendPasswordByEmail && StringUtils.isEmpty(user.getPassword())) {
            // Generate a password
            password = accessService.generatePassword(group);
            user.setPassword(hashHandler.hash(user.getSalt(), password));
            member.setUser(userDao.update(user));
            member.getMemberUser().setPasswordGenerated(true);
        }
        // Send activation mail
        mailHandler.sendActivation(activationMail == ActivationMail.THREADED, member, password);
    }

    private String toString(final Collection<Channel> channels) {
        StringBuilder str = new StringBuilder();
        for (Channel channel : channels) {
            channel = channelService.load(channel.getId());
            if (str.length() > 0) {
                str.append(", ");
            }
            str.append(channel.getDisplayName());
        }
        return str.toString();
    }

    /**
     * Updates end date of last group history log and create new group history log
     */
    private void updateGroupHistoryLogs(final Element element, final Group newGroup, final Calendar date) {
        // Update end date of last group history log
        final GroupHistoryLog lastGhl = groupHistoryLogDao.getLastGroupHistoryLog(element);
        if (lastGhl != null) {
            lastGhl.getPeriod().setEnd(date);
            groupHistoryLogDao.update(lastGhl);
        }

        // Create new group history log
        createGroupHistoryLog(element, newGroup, date);
    }
}