com.flexive.ejb.beans.AccountEngineBean.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.ejb.beans.AccountEngineBean.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library 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.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.ejb.beans;

import com.flexive.core.Database;
import com.flexive.core.DatabaseConst;
import com.flexive.core.LifeCycleInfoImpl;
import com.flexive.core.security.FxDBAuthentication;
import com.flexive.core.security.LoginLogoutHandler;
import com.flexive.core.security.UserTicketImpl;
import com.flexive.core.security.UserTicketStore;
import com.flexive.core.storage.DBStorage;
import com.flexive.core.storage.StorageManager;
import com.flexive.core.structure.StructureLoader;
import com.flexive.shared.*;
import com.flexive.shared.content.FxContent;
import com.flexive.shared.content.FxPK;
import com.flexive.shared.content.FxPermissionUtils;
import com.flexive.shared.exceptions.*;
import com.flexive.shared.interfaces.*;
import com.flexive.shared.scripting.FxScriptBinding;
import com.flexive.shared.scripting.FxScriptEvent;
import com.flexive.shared.security.*;
import com.flexive.shared.structure.FxType;
import com.flexive.shared.value.FxString;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.annotation.Resource;
import javax.ejb.*;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import static com.flexive.core.DatabaseConst.*;

/**
 * Account management
 *
 * @author Gregor Schober (gregor.schober@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
@Stateless(name = "AccountEngine", mappedName = "AccountEngine")
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
@TransactionManagement(TransactionManagementType.CONTAINER)
public class AccountEngineBean implements AccountEngine, AccountEngineLocal {

    private static final Log LOG = LogFactory.getLog(AccountEngineBean.class);

    /**
     * REST token expiry time
     */
    private static final int REST_TOKEN_EXPIRY = 60 * 60 * 1000; // 60 minutes
    /**
     * REST API token length
     */
    private static final int REST_TOKEN_LENGTH = 64;

    @Resource
    javax.ejb.SessionContext ctx;

    // Constant for the function checkPermissions(..)
    private static final int MAY_SET_ROLES = 1;
    // Constant for the function checkPermissions(..)
    private static final int MAY_SET_GROUPS = 2;
    // Constant for the function checkPermissions(..)
    private static final int MAY_UPDATE = 0;

    @EJB
    private UserGroupEngineLocal group;
    @EJB
    private LanguageEngineLocal language;
    @EJB
    private SequencerEngineLocal seq;
    @EJB
    private ContentEngineLocal co;
    @EJB
    private ScriptingEngineLocal scripting;

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<UserTicket> getActiveUserTickets() {
        return UserTicketStore.getTickets();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void login(String username, String password, boolean takeOver)
            throws FxLoginFailedException, FxAccountInUseException {
        DataSource ds;
        try {
            ds = Database.getDataSource();
        } catch (Exception exc) {
            throw new FxLoginFailedException(exc.getMessage(), FxLoginFailedException.TYPE_SQL_ERROR);
        }
        LoginLogoutHandler.doLogin(username, password, takeOver, ctx, ds);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void logout() throws FxLogoutFailedException {
        LoginLogoutHandler.doLogout();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public boolean loginCheck(String username, String password, UserTicket currentTicket)
            throws FxLoginFailedException, FxDbException {
        DataSource ds;
        boolean checkOK;
        try {
            ds = Database.getDataSource();
            checkOK = FxDBAuthentication.checkLogin(username, password, currentTicket, ds);
        } catch (Exception exc) {
            throw new FxLoginFailedException(exc.getMessage(), FxLoginFailedException.TYPE_SQL_ERROR);
        }
        return checkOK;
    }

    /**
     * Loads the account data from the database.
     *
     * @param loginName the unique name of the user, if null the accountId is used
     * @param accountId the unique id of the user to load
     * @param contactId the contact id
     * @return the Account object
     * @throws FxApplicationException if the user account could not be loaded
     */
    private Account load(String loginName, Long accountId, Long contactId) throws FxApplicationException {
        PreparedStatement stmt = null;
        String curSql;
        Connection con = null;
        try {

            con = Database.getDbConnection();
            // Load the account data
            curSql = "SELECT " +
            // 1, 2   ,  3        ,  4   ,  5       ,  6      ,  7      ,  8      ,  9
                    "ID,EMAIL,CONTACT_ID,MANDATOR,VALID_FROM,VALID_TO,DESCRIPTION,USERNAME,LOGIN_NAME," +
                    //10     ,  11        ,   12       ,    13    14          15         16
                    "IS_ACTIVE,IS_VALIDATED,LANG,ALLOW_MULTILOGIN,UPDATETOKEN,CREATED_BY,CREATED_AT," +
                    //17         18          19         20
                    "MODIFIED_BY,MODIFIED_AT,REST_TOKEN,REST_EXPIRES FROM " + TBL_ACCOUNTS + " WHERE "
                    + (loginName != null ? "UPPER(LOGIN_NAME)=?" : "")
                    + (accountId != null ? ((loginName != null) ? " AND " : "") + "ID=?" : "")
                    + (contactId != null
                            ? ((loginName != null || accountId != null) ? " AND " : "") + "CONTACT_ID=?"
                            : "");
            stmt = con.prepareStatement(curSql);

            int pos = 1;
            if (loginName != null) {
                stmt.setString(pos++, loginName.toUpperCase(Locale.ENGLISH));
            }
            if (accountId != null) {
                stmt.setLong(pos++, accountId);
            }
            if (contactId != null) {
                stmt.setLong(pos++, contactId);
            }

            ResultSet rs = stmt.executeQuery();
            if (rs == null || !rs.next())
                throw new FxNotFoundException("ex.account.notFound", (loginName != null ? loginName : accountId));
            long id = rs.getLong(1);
            String email = rs.getString(2);
            long contactDataId = rs.getLong(3);
            long mandator = rs.getLong(4);
            Date validFrom = new Date(rs.getLong(5));
            Date validTo = new Date(rs.getLong(6));
            String description = rs.getString(7);
            String name = rs.getString(8);
            String _loginName = rs.getString(9);
            boolean active = rs.getBoolean(10);
            boolean validated = rs.getBoolean(11);
            FxLanguage lang = language.load(rs.getInt(12));
            boolean bAllowMultiLogin = rs.getBoolean(13);
            String updateToken = rs.getString(14);
            String restToken = rs.getString(19);
            long restExpires = rs.getLong(20);
            Account ad = new Account(id, name, _loginName, mandator, email, lang, active, validated, validFrom,
                    validTo, -1, description, contactDataId, bAllowMultiLogin, updateToken, restToken, restExpires,
                    LifeCycleInfoImpl.load(rs, 15, 16, 17, 18));
            if (LOG.isDebugEnabled())
                LOG.debug(ad.toString());
            return ad;
        } catch (SQLException exc) {
            throw new FxLoadException(LOG, "ex.account.loadFailed.sql", (loginName != null ? loginName : accountId),
                    exc.getMessage());
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Account load(final long id) throws FxApplicationException {
        return load(null, id, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Account load(final String loginName) throws FxApplicationException {
        return load(loginName, null, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Account loadForContactData(FxPK contactDataPK) throws FxApplicationException {
        return load(null, null, contactDataPK.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String getPasswordHash(long accountId) throws FxApplicationException {
        if (!FxContext.getUserTicket().isGlobalSupervisor()) {
            throw new FxNoAccessException("ex.structure.noAccess.needSupervisor");
        }
        Connection con = null;
        PreparedStatement stmt = null;
        try {
            con = Database.getDbConnection();

            stmt = con.prepareStatement("SELECT password FROM " + TBL_ACCOUNTS + " WHERE id=?");
            stmt.setLong(1, accountId);

            final ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return rs.getString(1);
            } else {
                throw new FxNotFoundException("ex.account.notFound", accountId);
            }
        } catch (SQLException e) {
            throw new FxLoadException(LOG, e);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public UserTicket getUserTicket() {
        return UserTicketStore.getTicket();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public UserTicket getGuestTicket() {
        return UserTicketImpl.getGuestTicket();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<UserGroup> getGroups(long accountId) throws FxApplicationException {

        Connection con = null;
        Statement stmt = null;
        String curSql;
        final UserTicket ticket = getRequestTicket();
        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            // Check the user
            long mandator = getMandatorForAccount(con, accountId);
            // Permission checks (2)
            if (!ticket.isGlobalSupervisor()) {
                // Read access for all within a mandator
                if (mandator != ticket.getMandatorId())
                    throw new FxNoAccessException(LOG, "ex.account.groups.wrongMandator", accountId);
            }

            // Load the groups the account is assigned to
            curSql = "SELECT DISTINCT ass.USERGROUP,grp.NAME,grp.MANDATOR,grp.COLOR" + " FROM " + TBL_ASSIGN_GROUPS
                    + " ass, " + TBL_USERGROUPS + " grp" + " WHERE grp.ID=ass.USERGROUP AND ass.ACCOUNT="
                    + accountId;
            stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(curSql);
            final List<UserGroup> result = new ArrayList<UserGroup>(10);
            while (rs != null && rs.next()) {
                long id = rs.getLong(1);
                String grpName = rs.getString(2);
                long grpMandator = rs.getLong(3);
                String grpColor = rs.getString(4);
                final UserGroup aGroup = new UserGroup(id, grpName, grpMandator, grpColor);
                if (LOG.isDebugEnabled())
                    LOG.debug("Found group for user [" + accountId + "]: " + aGroup);
                result.add(aGroup);
            }
            return result;
        } catch (SQLException exc) {
            throw new FxLoadException(LOG, exc, "ex.account.groups.loadFailed.sql", accountId, exc.getMessage());
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }

    }

    /**
     * Internal helper function, returns the mandator of an account or throws a FxNotFoundException if the user does
     * not exist.
     *
     * @param con       a already opened connection, will not be closed by the function
     * @param accountId the account to check for
     * @return the mandator of the given account
     * @throws FxNotFoundException if the account does not exist
     * @throws SQLException        if the function failed because od a sql error
     */
    private long getMandatorForAccount(final Connection con, final long accountId)
            throws FxNotFoundException, SQLException {
        Statement stmt = null;
        try {
            String curSql = "SELECT MANDATOR FROM " + TBL_ACCOUNTS + " WHERE ID=" + accountId;
            stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(curSql);
            long mandator = -1;
            if (rs != null && rs.next())
                mandator = rs.getLong(1);
            if (mandator == -1)
                throw new FxNotFoundException(LOG, "ex.account.notFound", accountId);
            return mandator;
        } finally {
            Database.closeObjects(AccountEngineBean.class, null, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Role> getRoles(long accountId, RoleLoadMode mode) throws FxApplicationException {
        Connection con = null;
        Statement stmt = null;
        String curSql;
        final UserTicket ticket = getRequestTicket();

        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            // Check the user
            long mandator = getMandatorForAccount(con, accountId);
            // Permission checks (2)
            if (!ticket.isGlobalSupervisor()) {
                if (mandator != ticket.getMandatorId())
                    throw new FxNoAccessException("ex.account.roles.wrongMandator", accountId);
            }

            // Load the roles
            curSql = "SELECT DISTINCT ROLE FROM " + TBL_ROLE_MAPPING + " WHERE "
                    + ((mode == RoleLoadMode.ALL || mode == RoleLoadMode.FROM_USER_ONLY) ? "ACCOUNT=" + accountId
                            : "")
                    + ((mode == RoleLoadMode.ALL) ? " OR " : "")
                    + ((mode == RoleLoadMode.ALL || mode == RoleLoadMode.FROM_GROUPS_ONLY)
                            ? " USERGROUP IN (SELECT USERGROUP FROM "
                                    + TBL_ASSIGN_GROUPS + " WHERE ACCOUNT=" + accountId + " )"
                            : "");
            stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(curSql);
            List<Role> result = new ArrayList<Role>(15);
            while (rs != null && rs.next())
                result.add(Role.getById(rs.getByte(1)));

            if (LOG.isDebugEnabled())
                LOG.debug("Role for user [" + accountId + "]: " + result);
            return result;
        } catch (SQLException exc) {
            throw new FxLoadException(LOG, exc, "ex.account.roles.loadFailed.sql", accountId, exc.getMessage());
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public long create(Account account, String password) throws FxApplicationException {
        return create(account, password, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long create(Account account, String password, boolean hashPassword) throws FxApplicationException {
        final UserTicket ticket = FxContext.getUserTicket();
        return create(account.getName(), account.getLoginName(), password, account.getEmail(),
                account.getLanguage() != null ? account.getLanguage().getId() : FxLanguage.DEFAULT.getId(),
                account.getMandatorId() != -1 ? account.getMandatorId() : ticket.getMandatorId(),
                account.isActive(), account.isValidated(),
                account.getValidFrom() != null ? account.getValidFrom() : new Date(),
                account.getValidTo() != null ? account.getValidTo() : Account.VALID_FOREVER,
                account.getDefaultNode(), account.getDescription(), account.isAllowMultiLogin(), true,
                hashPassword);
    }

    private long create(String userName, String loginName, String password, String email, long lang,
            long mandatorId, boolean isActive, boolean isConfirmed, Date validFrom, Date validTo, long defaultNode,
            String description, boolean allowMultiLogin, boolean checkUserRoles, boolean hashPassword)
            throws FxApplicationException {

        final UserTicket ticket = FxContext.getUserTicket();
        // Security
        if (checkUserRoles && !ticket.isGlobalSupervisor()) {
            if (!ticket.isInRole(Role.MandatorSupervisor))
                FxPermissionUtils.checkRole(ticket, Role.AccountManagement);
            if (ticket.getMandatorId() != mandatorId)
                throw new FxNoAccessException(LOG, "ex.account.create.wrongMandator");
        }

        // Parameter checks
        try {
            CacheAdmin.getEnvironment().getMandator(mandatorId);
            loginName = checkLoginName(loginName);
            userName = checkUserName(userName);
            checkDates(validFrom, validTo);
            if (description == null)
                description = "";
            email = FxFormatUtils.checkEmail(email);
            if (!language.isValid(lang))
                throw new FxInvalidParameterException("LANGUAGE", "ex.account.languageInvalid", lang);
        } catch (FxInvalidParameterException pe) {
            if (LOG.isInfoEnabled())
                LOG.info(pe);
            throw pe;
        }

        Connection con = null;
        PreparedStatement stmt = null;
        String curSql;
        FxPK contactDataPK;
        try {
            final FxContext ri = FxContext.get();
            final FxContent contactData;
            try {
                if (!checkUserRoles) {
                    // we're probably running in unprivileged code, but application logic
                    // already determined that a user account should be created
                    ri.runAsSystem();
                }
                contactData = co.initialize(CacheAdmin.getEnvironment().getType(FxType.CONTACTDATA).getId());
            } finally {
                if (!checkUserRoles) {
                    ri.stopRunAsSystem();
                }
            }
            contactData.setAclId(ACL.ACL_CONTACTDATA);
            contactData.setValue("/SURNAME", new FxString(false, userName));
            contactData.setValue("/EMAIL", new FxString(false, email));
            try {
                FxContext.get().runAsSystem();
                contactDataPK = co.save(contactData);
            } finally {
                FxContext.get().stopRunAsSystem();
            }

            // Obtain a database connection
            con = Database.getDbConnection();

            // Get a new Id
            long newId = seq.getId(FxSystemSequencer.ACCOUNT);

            if (hashPassword) {
                password = password == null ? "" : FxFormatUtils.encodePassword(newId, userName, password);
            }

            curSql = "INSERT INTO " + TBL_ACCOUNTS + "(" +
            //1 2        3        4          5        6     7          8    9
                    "ID,MANDATOR,USERNAME,LOGIN_NAME,PASSWORD,EMAIL,CONTACT_ID,LANG,VALID_FROM," +
                    //10      11          12         13         14
                    "VALID_TO,DESCRIPTION,CREATED_BY,CREATED_AT,MODIFIED_BY," +
                    //15         16        17           18           19                20
                    "MODIFIED_AT,IS_ACTIVE,IS_VALIDATED,DEFAULT_NODE,ALLOW_MULTILOGIN,UPDATETOKEN) values " +
                    //1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
                    "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";

            final long NOW = System.currentTimeMillis();
            stmt = con.prepareStatement(curSql);
            stmt.setLong(1, newId);
            stmt.setLong(2, mandatorId);
            stmt.setString(3, userName);
            stmt.setString(4, loginName);
            stmt.setString(5, password);
            stmt.setString(6, email);
            stmt.setLong(7, contactDataPK.getId());
            stmt.setInt(8, (int) lang);
            //subtract a second to prevent login-after-immediatly-create problems with databases truncating milliseconds
            stmt.setLong(9, validFrom.getTime() - 1000);
            stmt.setLong(10, validTo.getTime());
            stmt.setString(11, description);
            stmt.setLong(12, ticket.getUserId());
            stmt.setLong(13, NOW);
            stmt.setLong(14, ticket.getUserId());
            stmt.setLong(15, NOW);
            stmt.setBoolean(16, isActive);
            stmt.setBoolean(17, isConfirmed);
            stmt.setLong(18, (defaultNode < 0) ? 0 : defaultNode);
            stmt.setBoolean(19, allowMultiLogin);
            stmt.setString(20, RandomStringUtils.randomAlphanumeric(64));
            stmt.executeUpdate();

            //make sure the user is the owner of his contact data
            stmt.close();
            stmt = con.prepareStatement("UPDATE " + TBL_CONTENT + " SET CREATED_BY=?, MANDATOR=? WHERE ID=?");
            stmt.setLong(1, newId);
            stmt.setLong(2, mandatorId);
            stmt.setLong(3, contactDataPK.getId());
            stmt.executeUpdate();
            CacheAdmin.expireCachedContent(contactDataPK.getId());

            // call scripts
            final List<Long> scriptIds = scripting.getByScriptEvent(FxScriptEvent.AfterAccountCreate);
            final FxScriptBinding binding = new FxScriptBinding();
            binding.setVariable("accountId", newId);
            binding.setVariable("pk", contactDataPK);
            for (long scriptId : scriptIds)
                scripting.runScript(scriptId, binding);

            StringBuilder sbHistory = new StringBuilder(1000);
            sbHistory.append("<account action=\"create\">\n").append("  <id>").append(newId).append("</id>\n")
                    .append("  <mandator>").append(CacheAdmin.getEnvironment().getMandator(mandatorId).getName())
                    .append("</mandator>\n").append("  <username>").append(userName).append("</username>\n")
                    .append("  <loginname>").append(loginName).append("</loginname>\n").append("  <email>")
                    .append(email).append("</email>\n").append("  <validfrom>")
                    .append(FxFormatUtils.toString(validFrom)).append("</validfrom>\n").append("  <validto>")
                    .append(FxFormatUtils.toString(validTo)).append("</validto>\n")
                    .append("  <description><![CDATA[").append(description).append("]]></description>\n")
                    .append("  <active>").append(isActive).append("</active>\n").append("  <confirmed>")
                    .append(isConfirmed).append("</confirmed>\n").append("  <multilogin>").append(allowMultiLogin)
                    .append("</multilogin>\n").append("</account>");
            EJBLookup.getHistoryTrackerEngine().trackData(sbHistory.toString(), "history.account.create",
                    loginName);

            StructureLoader.updateUserGroups(FxContext.get().getDivisionId(), group.loadAll(-1));

            // EVERY users is a member of group EVERYONE and his mandator
            final long[] groups = { UserGroup.GROUP_EVERYONE, group.loadMandatorGroup(mandatorId).getId() };
            ri.runAsSystem();
            try {
                setGroups(newId, groups);
            } catch (Exception exc) {
                throw new FxCreateException(exc, "ex.account.create.everyoneAssignFailed", exc.getMessage());
            } finally {
                ri.stopRunAsSystem();
            }

            // Return the id
            return newId;
        } catch (SQLException exc) {
            final boolean uniqueConstraintViolation = StorageManager.isUniqueConstraintViolation(exc);
            EJBUtils.rollback(ctx);
            if (uniqueConstraintViolation) {
                throw new FxEntryExistsException(LOG, exc, "ex.account.userExists", loginName, userName);
            } else {
                throw new FxCreateException(LOG, exc, "ex.account.createFailed.sql", loginName, exc.getMessage());
            }
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    private String checkLoginName(String name) throws FxInvalidParameterException {
        if (name == null || name.length() == 0)
            throw new FxInvalidParameterException("loginName", "ex.account.login.noName");
        if (name.length() > 255)
            throw new FxInvalidParameterException("loginName", "ex.account.login.nameTooLong", 255);
        return name.trim();
    }

    private String checkUserName(String name) throws FxInvalidParameterException {
        if (name == null || name.length() == 0)
            throw new FxInvalidParameterException("name", "ex.account.name.noName");
        if (name.length() > 255)
            throw new FxInvalidParameterException("name", "ex.account.name.nameTooLong", 255);
        return name.trim();
    }

    /**
     * Checks the dates.
     *
     * @param validFrom the from date
     * @param validTo   the to date
     * @throws FxInvalidParameterException if the dates are invalid
     */
    private void checkDates(final Date validFrom, final Date validTo) throws FxInvalidParameterException {
        if (validFrom == null)
            throw new FxInvalidParameterException("VALID_FROM", "ex.account.login.validFrom.missing");
        if (validTo == null)
            throw new FxInvalidParameterException("VALID_FROM", "ex.account.login.validTo.missing");
        if (validFrom.getTime() > validTo.getTime())
            throw new FxInvalidParameterException("VALID_FROM", "ex.account.login.dateMismatch");
        if (validTo.getTime() < java.lang.System.currentTimeMillis())
            throw new FxInvalidParameterException("VALID_TO", "ex.account.login.dateExpired");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void remove(long accountId) throws FxApplicationException {
        // Handle protected users
        if (accountId == Account.USER_GUEST)
            throw new FxNoAccessException("ex.account.delete.guest");
        if (accountId == Account.USER_GLOBAL_SUPERVISOR)
            throw new FxNoAccessException("ex.account.delete.supervisor");
        if (accountId == Account.NULL_ACCOUNT)
            throw new FxNoAccessException("ex.account.delete.nullAccount");

        Account account = load(accountId);

        // Permission checks
        final UserTicket ticket = FxContext.getUserTicket();
        if (!ticket.isGlobalSupervisor()) {
            if (!ticket.isInRole(Role.MandatorSupervisor))
                FxPermissionUtils.checkRole(ticket, Role.AccountManagement);
            if (account.getMandatorId() != ticket.getMandatorId())
                throw new FxNoAccessException(LOG, "ex.account.delete.wrongMandator");
        }

        Connection con = null;
        Statement stmt = null;
        String curSql = null;
        try {
            con = Database.getDbConnection();
            //Check if contents exist that the account created/modified
            stmt = con.createStatement();
            curSql = "SELECT COUNT(DISTINCT ID) FROM " + TBL_CONTENT + " WHERE CREATED_BY=" + accountId
                    + " OR MODIFIED_BY=" + accountId;
            ResultSet rs = stmt.executeQuery(curSql);
            if (rs != null && rs.next()) {
                if (rs.getLong(1) > 1) // 1 contact data record always exists
                    throw new FxNoAccessException("ex.account.delete.contentExists");
            }
            stmt.close();

            // also check all other tables with lifecycle information
            for (String tableName : TABLES_WITH_LCI) {
                if (!TBL_CONTENT.equals(tableName)) {
                    stmt = con.createStatement();
                    rs = stmt.executeQuery("SELECT COUNT(*) FROM " + tableName + " WHERE CREATED_BY=" + accountId
                            + " OR MODIFIED_BY=" + accountId);
                    if (rs.next() && rs.getLong(1) > 0) {
                        // TODO: set to special user account ("deleted user")?
                        LOG.warn("User " + accountId + " references table " + tableName + " (ignored)");
                    }
                }
            }

            // Delete all group assignments ..
            stmt = con.createStatement();
            curSql = "DELETE FROM " + TBL_ASSIGN_GROUPS + " WHERE ACCOUNT=" + accountId;
            stmt.executeUpdate(curSql);
            stmt.close();

            // Delete all role assigments ..
            stmt = con.createStatement();
            curSql = "DELETE FROM " + TBL_ROLE_MAPPING + " WHERE ACCOUNT=" + accountId;
            stmt.executeUpdate(curSql);
            stmt.close();

            // TODO: delete user specific configurations
            // TODO: delete user specific locks

            // Finally remove the user itself
            stmt = con.createStatement();
            curSql = "DELETE FROM " + TBL_ACCOUNTS + " WHERE ID=" + accountId;
            stmt.executeUpdate(curSql);

            // Log the user out of the system (if he is active)
            // This is done after the commit, since a failure should not block the user delete action
            UserTicketStore.removeUserId(accountId, null);

            // delete contact data
            try {
                // disable security since the contact data ACL prevents foreign users from deleting
                // CD instances. However, in this case the user has the role AccountManagement
                // which overrules the ACL permissions.
                FxContext.get().runAsSystem();
                co.remove(account.getContactData());
            } finally {
                FxContext.get().stopRunAsSystem();
            }
            EJBLookup.getHistoryTrackerEngine().track("history.account.remove", account.getLoginName());
        } catch (SQLException exc) {
            EJBUtils.rollback(ctx);
            throw new FxRemoveException(LOG, exc, "ex.account.delete.failed.sql", accountId, exc.getMessage(),
                    curSql);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * Returns wether the caller may use the operations 'edit','setRoles','setGroups'
     * on a specific user.
     *
     * @param accountId the id of the user to check for
     * @return a boolean array indicating if a action may be called<br>
     *         position <code>MAY_UPDATE</code>: update the user<br>
     *         position <code>MAY_SET_ROLES</code>: set roles for the user<br>
     *         position <code>MAY_SET_GROUPS</code>: set groups for the user
     * @throws FxApplicationException on errors
     */
    private boolean[] checkPermissions(long accountId) throws FxApplicationException {
        return _checkPermissions(load(accountId));
    }

    /**
     * Returns wether the caller may use the operations 'edit','setRoles','setGroups'
     * on a specific account.
     *
     * @param account the user to check the operations for
     * @return a boolean array indicating if a action may be called<br>
     *         position MAY_UPDATE: update the user<br>
     *         position MAY_SET_ROLES: set roles for the user<br>
     *         position MAY_SET_GROUPS: set groups for the user
     */
    private boolean[] _checkPermissions(Account account) {
        final UserTicket ticket = FxContext.getUserTicket();
        if (ticket.isGlobalSupervisor()
                || (ticket.getMandatorId() == account.getMandatorId() && ticket.isMandatorSupervisor())) {
            return new boolean[] { true, true, true }; // full access
        }

        boolean edit = true;
        boolean roles = true;
        boolean groups = true;

        // Edit access
        if (!ticket.isInRole(Role.AccountManagement))
            edit = false;
        if (ticket.getMandatorId() != account.getMandatorId())
            edit = false;

        // Update roles access
        if (!ticket.isInRole(Role.AccountManagement))
            roles = false;
        if (ticket.getMandatorId() != account.getMandatorId())
            roles = false;

        // Update groups access. Specified via AccountManagement
        if (!ticket.isInRole(Role.AccountManagement))
            groups = false;
        if (ticket.getMandatorId() != account.getMandatorId())
            groups = false;

        boolean result[] = new boolean[3];
        result[MAY_SET_GROUPS] = groups;
        result[MAY_SET_ROLES] = roles;
        result[MAY_UPDATE] = edit;
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void setGroups(long accountId, List<UserGroup> groups) throws FxApplicationException {
        if (groups != null && groups.size() > 0) {
            long groupIds[] = new long[groups.size()];
            int pos = 0;
            for (UserGroup group : groups) {
                groupIds[pos++] = group.getId();
            }
            setGroups(accountId, groupIds);
        } else {
            setGroups(accountId);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void setGroups(long accountId, long... groups) throws FxApplicationException {
        // Handle null params
        if (groups == null)
            groups = new long[0];

        final UserTicket ticket = FxContext.getUserTicket();

        // Permission checks
        try {
            if (!checkPermissions(accountId)[MAY_SET_GROUPS])
                throw new FxNoAccessException("ex.account.groups.noAssignPermission", accountId);
        } catch (FxLoadException le) {
            throw new FxUpdateException(le.getMessage());
        }

        Account account = load(accountId);

        // Remove duplicated entries and add group everyone
        groups = FxArrayUtils.removeDuplicates(groups);

        // Ensure group EVERYONE and mandator group is part of the list, since every user is assigned to it
        groups = FxArrayUtils.addElement(groups, UserGroup.GROUP_EVERYONE);
        groups = FxArrayUtils.addElement(groups, group.loadMandatorGroup(account.getMandatorId()).getId());

        // Check the groups
        List<UserGroup> currentlyAssigned;
        try {
            currentlyAssigned = getGroups(accountId);
        } catch (FxLoadException exc) {
            throw new FxUpdateException(exc);
        }

        StringBuilder sbHistory = new StringBuilder(1000);
        sbHistory.append("<original>\n");
        for (UserGroup org : currentlyAssigned)
            sbHistory.append("  <group id=\"").append(org.getId()).append("\" mandator=\"")
                    .append(org.getMandatorId()).append("\">").append(org.getName()).append("</group>\n");
        sbHistory.append("</original>\n").append("<new>\n");

        for (long grp : groups) {
            UserGroup g = CacheAdmin.getEnvironment().getUserGroup(grp);
            sbHistory.append("  <group id=\"").append(g.getId()).append("\" mandator=\"").append(g.getMandatorId())
                    .append("\">").append(g.getName()).append("</group>\n");
            // Do not check the special gropups
            if (grp == UserGroup.GROUP_EVERYONE)
                continue;
            if (grp == UserGroup.GROUP_OWNER)
                continue;
            if (grp == UserGroup.GROUP_UNDEFINED)
                continue;
            if (FxSharedUtils.indexOfSelectableObject(currentlyAssigned, grp) != -1)
                continue;
            // Perform the check for all regular groups, loading an inaccessible
            // group will throw an exception which is what we want here

            if (g.isSystem())
                continue;
            //make sure the calling user is assigned all roles of the group as well
            for (Role check : group.getRoles(grp))
                if (!ticket.isInRole(check))
                    throw new FxNoAccessException("ex.account.roles.assign.noMember.group", check.name(),
                            g.getName());
            if (!ticket.isGlobalSupervisor()) {
                if (g.getMandatorId() != account.getMandatorId())
                    throw new FxNoAccessException("ex.account.group.assign.wrongMandator", g.getName(), accountId);
            }
        }
        sbHistory.append("</new>\n");

        // Write group assignments to the database
        Connection con = null;
        PreparedStatement ps = null;

        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            // Delete the old assignments of the user
            ps = con.prepareStatement("DELETE FROM " + TBL_ASSIGN_GROUPS + " WHERE ACCOUNT=?");
            ps.setLong(1, accountId);
            ps.executeUpdate();
            if (groups.length > 0) {
                ps.close();
                ps = con.prepareStatement("INSERT INTO " + TBL_ASSIGN_GROUPS + " (ACCOUNT,USERGROUP) VALUES (?,?)");
            }

            // Store the new assignments of the user
            for (long group : groups) {
                // Skipp 'null' group
                if (group == UserGroup.GROUP_UNDEFINED)
                    continue;
                ps.setLong(1, accountId);
                ps.setLong(2, group);
                ps.executeUpdate();
            }
            LifeCycleInfoImpl.updateLifeCycleInfo(TBL_ACCOUNTS, "ID", accountId);
            // Ensure any active ticket of the updated user are refreshed
            UserTicketStore.flagDirtyHavingUserId(accountId);
            EJBLookup.getHistoryTrackerEngine().trackData(sbHistory.toString(), "history.account.setGroups",
                    account.getLoginName());
        } catch (SQLException exc) {
            EJBUtils.rollback(ctx);
            throw new FxUpdateException(LOG, exc, "ex.account.roles.updateFailed.sql", accountId, exc.getMessage());
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, ps);
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void setRoles(long accountId, List<Role> roles) throws FxApplicationException {
        if (roles != null && roles.size() > 0) {
            setRoles(accountId, roles.toArray(new Role[roles.size()]));
        } else {
            setRoles(accountId);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void setRoles(long accountId, long... roles) throws FxApplicationException {
        if (roles == null)
            roles = new long[0];
        final Account account = load(accountId);

        if (!_checkPermissions(account)[MAY_SET_ROLES])
            throw new FxNoAccessException("ex.account.roles.noAssignPermission", accountId);

        // Write roles to database
        Connection con = null;
        PreparedStatement ps = null;

        StringBuilder sbHistory = new StringBuilder(1000);

        try {
            roles = FxArrayUtils.removeDuplicates(roles);
            con = Database.getDbConnection();

            UserTicket ticket = FxContext.getUserTicket();
            List<Role> orgRoles = getRoles(accountId, RoleLoadMode.FROM_USER_ONLY);
            sbHistory.append("<original>\n");
            for (Role org : orgRoles)
                sbHistory.append("  <role id=\"").append(org.getId()).append("\">").append(org.getName())
                        .append("</role>\n");
            sbHistory.append("</original>\n");
            //only allow to assign roles which the calling user is a member of (unless it is a global supervisor)
            if (!ticket.isGlobalSupervisor()
                    && !(ticket.isMandatorSupervisor() && account.getMandatorId() == ticket.getMandatorId())) {
                final List<Long> orgRoleIds = FxSharedUtils.getSelectableObjectIdList(orgRoles);
                //check removed roles
                for (long check : orgRoleIds) {
                    if (!ArrayUtils.contains(roles, check)) {
                        if (!ticket.isInRole(Role.getById(check))) {
                            EJBUtils.rollback(ctx);
                            throw new FxNoAccessException("ex.account.roles.assign.noMember.remove",
                                    Role.getById(check).getName());
                        }
                    }
                }
                //check added roles
                for (long check : roles) {
                    if (!orgRoleIds.contains(check)) {
                        if (!ticket.isInRole(Role.getById(check))) {
                            EJBUtils.rollback(ctx);
                            throw new FxNoAccessException("ex.account.roles.assign.noMember.add",
                                    Role.getById(check).getName());
                        }
                    }
                }
            }

            // Delete the old assignments of the user
            ps = con.prepareStatement("DELETE FROM " + TBL_ROLE_MAPPING + " WHERE ACCOUNT=?");
            ps.setLong(1, accountId);
            ps.executeUpdate();

            if (roles.length > 0) {
                ps.close();
                ps = con.prepareStatement(
                        "INSERT INTO " + TBL_ROLE_MAPPING + " (ACCOUNT,USERGROUP,ROLE) VALUES (?,?,?)");
            }

            sbHistory.append("<new>\n");
            // Store the new assignments of the account
            for (long role : roles) {
                if (Role.isUndefined(role))
                    continue;
                ps.setLong(1, accountId);
                ps.setLong(2, UserGroup.GROUP_NULL);
                ps.setLong(3, role);
                ps.executeUpdate();
                sbHistory.append("  <role id=\"").append(role).append("\">").append(Role.getById(role).getName())
                        .append("</role>\n");
            }
            sbHistory.append("</new>\n");
            LifeCycleInfoImpl.updateLifeCycleInfo(TBL_ACCOUNTS, "ID", accountId);
            // Ensure any active ticket of the updated account are refreshed
            UserTicketStore.flagDirtyHavingUserId(accountId);
            EJBLookup.getHistoryTrackerEngine().trackData(sbHistory.toString(), "history.account.setRoles",
                    account.getLoginName());
        } catch (SQLException exc) {
            EJBUtils.rollback(ctx);
            throw new FxUpdateException(LOG, exc, "ex.account.roles.updateFailed.sql", accountId, exc.getMessage());
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, ps);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void addRole(long accountId, long roleId) throws FxApplicationException {
        final List<Role> roles = new ArrayList<Role>();
        roles.addAll(getRoles(accountId, RoleLoadMode.ALL));
        roles.add(Role.getById(roleId));
        setRoles(accountId, roles);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void addGroup(long accountId, long groupId) throws FxApplicationException {
        final List<UserGroup> groups = new ArrayList<UserGroup>();
        groups.addAll(getGroups(accountId));
        groups.add(CacheAdmin.getEnvironment().getUserGroup(groupId));
        setGroups(accountId, groups);
    }

    /**
     * Helper function to set the roles for an account.
     *
     * @param accountId the account id
     * @param roles     the roles to set
     * @throws FxApplicationException on errors
     */
    private void setRoles(long accountId, Role[] roles) throws FxApplicationException {
        long[] roleIds = new long[roles.length];
        for (int i = 0; i < roles.length; i++) {
            roleIds[i] = roles[i].getId();
        }
        setRoles(accountId, roleIds);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Account> loadAll(String name, String loginName, String email, Boolean isActive, Boolean isConfirmed,
            Long mandatorId, int[] isInRole, long[] isInGroup, int startIdx, int maxEntries)
            throws FxApplicationException {

        Connection con = null;
        Statement stmt = null;
        final UserTicket ticket = FxContext.getUserTicket();

        String curSql = _buildSearchStmt(ticket, name, loginName, email, isActive, isConfirmed, mandatorId,
                isInRole, isInGroup, false);

        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            // Load the users
            stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(curSql);
            ArrayList<Account> alResult = new ArrayList<Account>(1000);
            int counter = 0;
            if (startIdx < 0)
                startIdx = 0;
            if (rs != null) {
                while (rs.next()) {

                    // jump to startIndex
                    if (startIdx > 0) {
                        startIdx--;
                        continue;
                    }

                    // return only maxEntries
                    if (counter > -1) {
                        if (counter == maxEntries)
                            break;
                        counter++;
                    }

                    // add to result
                    long id = rs.getLong(1);
                    String _email = rs.getString(2);
                    long contactDataId = rs.getLong(3);
                    if (rs.wasNull())
                        contactDataId = -1;
                    long mandator = rs.getLong(4);
                    FxLanguage lang = language.load(rs.getInt(5));
                    Date validFrom = new Date(rs.getLong(6));
                    Date validTo = new Date(rs.getLong(7));
                    String description = rs.getString(8);
                    String _name = rs.getString(9);
                    String _loginName = rs.getString(10);
                    boolean active = rs.getBoolean(11);
                    boolean validated = rs.getBoolean(12);
                    boolean multiLogin = rs.getBoolean(13);
                    String updateToken = rs.getString(14);
                    String restToken = rs.getString(19);
                    long restTokenExpires = rs.getLong(20);

                    alResult.add(new Account(id, _name, _loginName, mandator, _email, lang, active, validated,
                            validFrom, validTo, -1, description, contactDataId, multiLogin, updateToken, restToken,
                            restTokenExpires, LifeCycleInfoImpl.load(rs, 15, 16, 17, 18)));
                }
            }

            return alResult;
        } catch (FxInvalidLanguageException exc) {
            throw new FxLoadException(exc);
        } catch (SQLException exc) {
            throw new FxLoadException(LOG, exc, "ex.account.loadAll.failed.sql", exc.getMessage(), curSql);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Account> loadAll(long mandatorId) throws FxApplicationException {
        return loadAll(null, null, null, null, null, mandatorId, null, null, 0, Integer.MAX_VALUE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Account> loadAll() throws FxApplicationException {
        final UserTicket ticket = FxContext.getUserTicket();
        return loadAll(null, null, null, null, null, ticket.isGlobalSupervisor() ? null : ticket.getMandatorId(),
                null, null, 0, Integer.MAX_VALUE);
    }

    /**
     * Helper function.
     *
     * @param ticket      the ticket of the calling user
     * @param name        the name filter
     * @param loginName   the login name filter
     * @param email       the email filter
     * @param isActive    the is active filter
     * @param isConfirmed the is confirmed filter
     * @param mandatorId  the mandator id filter
     * @param isInRole    the role filter
     * @param isInGroup   the group filter
     * @param isCountOnly the "count only" option
     * @return the sql filter String
     * @throws FxNoAccessException if the user does not have access
     */
    private String _buildSearchStmt(UserTicket ticket, String name, String loginName, String email,
            Boolean isActive, Boolean isConfirmed, Long mandatorId, int[] isInRole, long[] isInGroup,
            boolean isCountOnly) throws FxNoAccessException {

        // Do no filter GROUP_UNDEFINED (=null value)
        if (isInGroup != null) {
            isInGroup = ArrayUtils.removeElement(isInGroup, UserGroup.GROUP_UNDEFINED);
        }

        // Determine if group/role filter options are enabled
        final boolean filterByGrp = (isInGroup != null && isInGroup.length > 0);
        final boolean filterByRole = (isInRole != null && isInRole.length > 0);

        // Determine the mandator, and check its security
        long _mandatorId = (mandatorId == null) ? ticket.getMandatorId() : mandatorId;
        if (_mandatorId < 0 && !ticket.isGlobalSupervisor())
            throw new FxNoAccessException(LOG, "ex.account.loadFailed.wrongMandator");

        // Do case insensitive search
        if (email != null)
            email = email.toUpperCase();
        if (name != null)
            name = name.toUpperCase();
        if (loginName != null)
            loginName = loginName.toUpperCase();

        final String TRUE = StorageManager.getBooleanTrueExpression();
        String curSql = "SELECT " + "" + (isCountOnly ? "COUNT(*)" :
        //   1      2         3              4            5        6
                "usr.ID,usr.EMAIL,usr.CONTACT_ID,usr.MANDATOR,usr.LANG,usr.VALID_FROM," +
                //   7            8               9            10             11
                        "usr.VALID_TO,usr.DESCRIPTION,usr.USERNAME,usr.LOGIN_NAME,usr.IS_ACTIVE," +
                        //   12               13                   14
                        "usr.IS_VALIDATED,usr.ALLOW_MULTILOGIN,usr.UPDATETOKEN," +
                        //   15             16             17              18
                        "usr.CREATED_BY,usr.CREATED_AT,usr.MODIFIED_BY,usr.MODIFIED_AT," +
                        // 19           20
                        "usr.REST_TOKEN,usr.REST_EXPIRES")
                + " FROM " + TBL_ACCOUNTS + " usr WHERE ID!=" + Account.NULL_ACCOUNT + " "
                + ((_mandatorId == -1) ? ""
                        : " AND (usr.MANDATOR=" + _mandatorId + " OR usr.ID<=" + Account.USER_GLOBAL_SUPERVISOR
                                + ")")
                + ((name != null && name.length() > 0) ? " AND UPPER(usr.USERNAME) LIKE '%" + name + "%'" : "")
                + ((loginName != null && loginName.length() > 0)
                        ? " AND UPPER(usr.LOGIN_NAME) LIKE '%" + loginName + "%'"
                        : "")
                + ((email != null && email.length() > 0) ? " AND UPPER(usr.EMAIL) LIKE '%" + email + "%'" : "")
                + ((isActive != null) ? " AND usr.IS_ACTIVE" + (isActive ? "=" : "<>") + TRUE : "")
                + ((isConfirmed != null) ? " AND usr.IS_VALIDATED" + (isConfirmed ? "=" : "<>") + TRUE : "") +

                // Group link
                ((!filterByGrp) ? ""
                        : " AND EXISTS (SELECT 1 FROM " + TBL_ASSIGN_GROUPS
                                + " grp WHERE grp.ACCOUNT=usr.ID AND grp.USERGROUP IN ("
                                + StringUtils.join(ArrayUtils.toObject(isInGroup), ',') + "))")
                +

                // Role link
                ((!filterByRole) ? ""
                        : " AND (EXISTS (SELECT 1 FROM " + TBL_ROLE_MAPPING
                                + " rol WHERE rol.ACCOUNT=usr.ID and rol.ROLE IN ("
                                + StringUtils.join(ArrayUtils.toObject(isInRole), ',') + ")) OR "
                                + "EXISTS (SELECT 1 FROM " + TBL_ROLE_MAPPING + " rol, " + TBL_ASSIGN_GROUPS
                                + " grp WHERE "
                                + "grp.ACCOUNT=usr.ID AND rol.USERGROUP=grp.USERGROUP AND rol.ROLE IN ("
                                + StringUtils.join(ArrayUtils.toObject(isInRole), ',') + ")))")
                +

                // Order
                (isCountOnly ? "" : " ORDER by usr.LOGIN_NAME");
        if (LOG.isDebugEnabled())
            LOG.debug(curSql);
        return curSql;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public int getAccountMatches(String name, String loginName, String email, Boolean isActive, Boolean isConfirmed,
            Long mandatorId, int[] isInRole, long[] isInGroup) throws FxApplicationException {

        Connection con = null;
        Statement stmt = null;
        final UserTicket ticket = FxContext.getUserTicket();
        String curSql = _buildSearchStmt(ticket, name, loginName, email, isActive, isConfirmed, mandatorId,
                isInRole, isInGroup, true);
        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            // Load the users
            stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(curSql);
            if (rs != null && rs.next())
                return rs.getInt(1);
            return -1;

        } catch (SQLException exc) {
            throw new FxLoadException(LOG, exc, "ex.account.loadAll.failed.sql", exc.getMessage(), curSql);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void update(long accountId, String password, Long defaultNode, String name, String loginName,
            String email, Boolean isConfirmed, Boolean isActive, Date validFrom, Date validTo, Long lang,
            String description, Boolean allowMultiLogin, Long contactDataId) throws FxApplicationException {

        // Load the account to update
        Account account = load(accountId);

        StringBuilder sbHistory = new StringBuilder(1000);
        sbHistory.append("<original>\n").append("  <id>").append(accountId).append("</id>\n").append("  <mandator>")
                .append(CacheAdmin.getEnvironment().getMandator(account.getMandatorId()).getName())
                .append("</mandator>\n").append("  <username>").append(account.getName()).append("</username>\n")
                .append("  <loginname>").append(account.getLoginName()).append("</loginname>\n").append("  <email>")
                .append(account.getEmail()).append("</email>\n").append("  <validfrom>")
                .append(account.getValidFromString()).append("</validfrom>\n").append("  <validto>")
                .append(account.getValidToString()).append("</validto>\n").append("  <description><![CDATA[")
                .append(account.getDescription()).append("]]></description>\n").append("  <active>")
                .append(account.isActive()).append("</active>\n").append("  <confirmed>")
                .append(account.isValidated()).append("</confirmed>\n").append("  <multilogin>")
                .append(account.isAllowMultiLogin()).append("</multilogin>\n").append("</original>\n");

        final UserTicket ticket = FxContext.getUserTicket();
        // Determine if only fields are accessed that the use may alter for himself
        final boolean protectedFields = (name != null || loginName != null || isConfirmed != null
                || isActive != null || validTo != null || validFrom != null || description != null);
        if (!protectedFields && ticket.getUserId() == accountId) {
            // passed
        } else {
            if (!_checkPermissions(account)[MAY_UPDATE])
                throw new FxNoAccessException(LOG, "ex.account.update.noPermission", account.getName());
        }

        // Parameter checks
        try {
            if (loginName != null)
                loginName = checkLoginName(loginName);
            if (email != null)
                email = FxFormatUtils.checkEmail(email);
            if (password != null) {
                password = FxFormatUtils.encodePassword(accountId,
                        StringUtils.defaultString(loginName, account.getLoginName()), password.trim());
            }
            if (lang != null && !language.isValid(lang))
                throw new FxInvalidParameterException("LANGUAGE", "ex.account.languageInvalid", lang);
        } catch (FxInvalidParameterException pe) {
            if (LOG.isInfoEnabled())
                LOG.info(pe);
            throw pe;
        }

        Connection con = null;
        PreparedStatement stmt = null;
        String curSql;

        if (name == null)
            name = account.getName();
        if (loginName == null)
            loginName = account.getLoginName();
        if (email == null)
            email = account.getEmail();
        if (lang == null)
            lang = account.getLanguage().getId();
        if (isActive == null)
            isActive = account.isActive();
        if (isConfirmed == null)
            isConfirmed = account.isValidated();
        if (description == null)
            description = account.getDescription();
        if (defaultNode == null)
            defaultNode = account.getDefaultNode();
        if (allowMultiLogin == null)
            allowMultiLogin = account.isAllowMultiLogin();
        // Assign and check dates
        if (validFrom == null)
            validFrom = account.getValidFrom();
        if (validTo == null)
            validTo = account.getValidTo();
        checkDates(validFrom, validTo);
        if (defaultNode < 0)
            defaultNode = (long) 0;

        try {

            // Obtain a database connection
            con = Database.getDbConnection();

            curSql = "UPDATE " + TBL_ACCOUNTS + " SET " +
            // 1          2            3            4           5               6
                    "EMAIL=?,LANG=?,VALID_FROM=?,VALID_TO=?,DESCRIPTION=?," +
                    // 6                  7             8            9              10
                    "MODIFIED_BY=?,MODIFIED_AT=?,IS_ACTIVE=?,IS_VALIDATED=?,DEFAULT_NODE=?," +
                    //  11,        12     ,    13      ,                                14
                    "USERNAME=?,LOGIN_NAME=?,ALLOW_MULTILOGIN=?" + ((password != null) ? ",PASSWORD=?" : "")
                    + ((contactDataId != null) ? ",CONTACT_ID=?" : "") + " WHERE ID=" + accountId;
            stmt = con.prepareStatement(curSql);
            stmt.setString(1, email);
            stmt.setInt(2, lang.intValue());
            stmt.setLong(3, validFrom.getTime());
            stmt.setLong(4, validTo.getTime());
            stmt.setString(5, description);
            stmt.setLong(6, ticket.getUserId());
            stmt.setLong(7, System.currentTimeMillis());
            stmt.setBoolean(8, isActive);
            stmt.setBoolean(9, isConfirmed);
            stmt.setLong(10, defaultNode);
            stmt.setString(11, name);
            stmt.setString(12, loginName);
            stmt.setBoolean(13, allowMultiLogin);
            int pos = 14;
            if (password != null)
                stmt.setString(pos++, password);
            if (contactDataId != null)
                stmt.setLong(pos/*++*/, contactDataId);
            stmt.executeUpdate();
            if (contactDataId != null) {
                //make sure the user is the owner of his contact data
                stmt.close();
                stmt = con.prepareStatement("UPDATE " + TBL_CONTENT + " SET CREATED_BY=? WHERE ID=?");
                stmt.setLong(1, accountId);
                stmt.setLong(2, contactDataId);
                stmt.executeUpdate();
            }

            // Log the user out of the system if he was made active
            if (!isActive || !isConfirmed) {
                UserTicketStore.removeUserId(accountId, null);
            } else {
                // Ensure any active ticket of the updated user are refreshed
                UserTicketStore.flagDirtyHavingUserId(account.getId());
            }

            sbHistory.append("<new>\n").append("  <id>").append(accountId).append("</id>\n").append("  <mandator>")
                    .append(CacheAdmin.getEnvironment().getMandator(account.getMandatorId()).getName())
                    .append("</mandator>\n").append("  <username>").append(name).append("</username>\n")
                    .append("  <loginname>").append(loginName).append("</loginname>\n").append("  <email>")
                    .append(email).append("</email>\n").append("  <validfrom>")
                    .append(FxFormatUtils.toString(validFrom)).append("</validfrom>\n").append("  <validto>")
                    .append(FxFormatUtils.toString(validTo)).append("</validto>\n")
                    .append("  <description><![CDATA[").append(description).append("]]></description>\n")
                    .append("  <active>").append(isActive).append("</active>\n").append("  <confirmed>")
                    .append(isConfirmed).append("</confirmed>\n").append("  <multilogin>").append(allowMultiLogin)
                    .append("</multilogin>\n").append("</new>");
            EJBLookup.getHistoryTrackerEngine().trackData(sbHistory.toString(), "history.account.update",
                    account.getLoginName());
        } catch (SQLException exc) {
            final boolean uniqueConstraintViolation = StorageManager.isUniqueConstraintViolation(exc);
            EJBUtils.rollback(ctx);
            if (uniqueConstraintViolation) {
                throw new FxEntryExistsException(LOG, "ex.account.userExists", name, loginName);
            } else {
                throw new FxUpdateException(LOG, "ex.account.update.failed.sql", account.getLoginName(),
                        exc.getMessage());
            }
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void updateUser(long accountId, String password, String name, String loginName, String email, Long lang)
            throws FxApplicationException {
        _updateUser(accountId, password, true, name, loginName, email, lang);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void updateUser(long accountId, String password, boolean hashPassword, String name, String loginName,
            String email, Long lang) throws FxApplicationException {
        _updateUser(accountId, password, hashPassword, name, loginName, email, lang);
    }

    private void _updateUser(long accountId, String password, boolean hashPassword, String name, String loginName,
            String email, Long lang) throws FxApplicationException {
        // Load the account to update
        Account account = load(accountId);

        final UserTicket ticket = FxContext.getUserTicket();
        if (ticket.getUserId() != accountId) {
            if (!ticket.isGlobalSupervisor()
                    || !(ticket.isMandatorSupervisor() && account.getMandatorId() == ticket.getMandatorId()))
                throw new FxNoAccessException(LOG, "ex.account.update.noPermission", account.getName());
        }

        // Parameter checks
        if (loginName != null)
            loginName = checkLoginName(loginName);
        if (email != null)
            email = FxFormatUtils.checkEmail(email);
        if (password != null && hashPassword) {
            password = FxFormatUtils.encodePassword(accountId, account.getLoginName(), password.trim());
        }

        if (lang != null && !language.isValid(lang))
            throw new FxInvalidParameterException("LANGUAGE", "ex.account.languageInvalid", lang);

        Connection con = null;
        PreparedStatement stmt = null;
        String curSql;

        if (name == null)
            name = account.getName();
        if (loginName == null)
            loginName = account.getLoginName();
        if (email == null)
            email = account.getEmail();
        if (lang == null)
            lang = account.getLanguage().getId();

        try {
            // Obtain a database connection
            con = Database.getDbConnection();

            curSql = "UPDATE " + TBL_ACCOUNTS + " SET " +
            // 1      2           3            4              5             6                                    7
                    "EMAIL=?,LANG=?, USERNAME=?,LOGIN_NAME=?, MODIFIED_BY=?,MODIFIED_AT=?"
                    + ((password != null) ? ",PASSWORD=?" : "") + " WHERE ID=" + accountId;
            stmt = con.prepareStatement(curSql);
            stmt.setString(1, email);
            stmt.setInt(2, lang.intValue());
            stmt.setString(3, name);
            stmt.setString(4, loginName);

            stmt.setLong(5, ticket.getUserId());
            stmt.setLong(6, System.currentTimeMillis());

            if (password != null)
                stmt.setString(7, password);
            stmt.executeUpdate();

            // Ensure any active ticket of the updated user are refreshed
            UserTicketStore.flagDirtyHavingUserId(account.getId());
        } catch (SQLException exc) {
            final boolean uniqueConstraintViolation = StorageManager.isUniqueConstraintViolation(exc);
            EJBUtils.rollback(ctx);
            if (uniqueConstraintViolation) {
                throw new FxEntryExistsException(LOG, "ex.account.userExists", name, loginName);
            } else {
                throw new FxUpdateException(LOG, "ex.account.update.failed.sql", account.getLoginName(),
                        exc.getMessage());
            }
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Account> getAssignedUsers(long groupId, int startIdx, int maxEntries)
            throws FxApplicationException {

        final UserTicket ticket = FxContext.getUserTicket();

        // Load the requested group
        final UserGroup theGroup = CacheAdmin.getEnvironment().getUserGroup(groupId);

        long[] groups = { theGroup.getId() };

        // Return the users from ALL mandators assigned to the group if the caller is GLOBAL_SUPERVISOR.
        // Else only look for users within the callers mandator.
        Long mandatorId = ticket.isGlobalSupervisor() ? -1 : ticket.getMandatorId();

        // Load the users

        return loadAll(null, null, null, null, null, mandatorId, null, groups, startIdx, maxEntries);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public long getAssignedUsersCount(long groupId, boolean includeInvisible) throws FxApplicationException {

        if (groupId < 0 || groupId == UserGroup.GROUP_UNDEFINED)
            return 0;
        final UserTicket ticket = FxContext.getUserTicket();
        Connection con = null;
        Statement stmt = null;
        String sCurSql = null;
        try {

            // Obtain a database connection
            con = Database.getDbConnection();

            // Nothing is invisible for the GLOBAL_SUPERVISOR
            if (ticket.isGlobalSupervisor())
                includeInvisible = true;

            // Load the count
            stmt = con.createStatement();

            if (includeInvisible) {
                sCurSql = "SELECT COUNT(*) FROM " + TBL_ASSIGN_GROUPS + " WHERE USERGROUP=" + groupId;
            } else {
                sCurSql = "SELECT COUNT(*) FROM " + TBL_ASSIGN_GROUPS + " ln, " + TBL_ACCOUNTS
                        + " usr WHERE ln.ACCOUNT=usr.ID AND usr.MANDATOR=" + ticket.getMandatorId()
                        + " AND ln.USERGROUP=" + groupId;
            }

            ResultSet rs = stmt.executeQuery(sCurSql);
            long result = 0;
            if (rs != null && rs.next())
                result = rs.getLong(1);

            if (LOG.isDebugEnabled())
                LOG.debug("Users in group [" + groupId + "]:" + result + ", caller=" + ticket);
            return result;
        } catch (SQLException exc) {
            FxLoadException ce = new FxLoadException(
                    "Database error! Last stmt was [" + sCurSql + "]:" + exc.getMessage(), exc);
            LOG.error(ce);
            throw ce;
        } finally {
            Database.closeObjects(UserGroupEngineBean.class, con, stmt);
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<ACLAssignment> loadAccountAssignments(long accountId) throws FxApplicationException {
        Connection con = null;
        PreparedStatement stmt = null;
        String curSql;
        UserTicket ticket = getRequestTicket();

        // Security checks
        if (!ticket.isGlobalSupervisor()
                && (!(accountId == ticket.getUserId() || accountId == Account.USER_GUEST))) {
            try {
                Account usr = load(accountId);
                if (ticket.isMandatorSupervisor() && ticket.getMandatorId() == usr.getMandatorId()) {
                    // MandatorSupervisor may access all users within his domain
                } else {
                    FxNoAccessException nae = new FxNoAccessException(
                            "You may not access the ACLAssignment of user [" + accountId + "]");
                    if (LOG.isInfoEnabled())
                        LOG.info(nae);
                    throw nae;
                }
            } catch (FxNotFoundException exc) {
                return new ArrayList<ACLAssignment>(0);
            }
        }

        try {

            // Obtain a database connection
            con = Database.getDbConnection();

            // Fetch assignments
            //                            1             2       3         4         5           6           7
            curSql = "SELECT DISTINCT ass.USERGROUP,ass.ACL,ass.PREAD,ass.PEDIT,ass.PREMOVE,ass.PEXPORT,ass.PREL," +
            //   8
                    "ass.PCREATE, " +
                    // 9
                    "(SELECT acl.CAT_TYPE FROM " + TBL_ACLS + " acl WHERE acl.ID=ass.ACL)" +
                    // 10             11             12              13
                    ",ass.CREATED_BY,ass.CREATED_AT,ass.MODIFIED_BY,ass.MODIFIED_AT " + "FROM "
                    + TBL_ACLS_ASSIGNMENT + " ass " + "WHERE ass.USERGROUP IN (SELECT grp.USERGROUP FROM "
                    + TBL_ASSIGN_GROUPS + " grp WHERE grp.ACCOUNT=?)" + " OR ass.USERGROUP="
                    + UserGroup.GROUP_OWNER;

            stmt = con.prepareStatement(curSql);
            stmt.setLong(1, accountId);
            ResultSet rs = stmt.executeQuery();

            // Read the data
            List<ACLAssignment> result = new ArrayList<ACLAssignment>(50);
            while (rs != null && rs.next()) {
                long groupId = rs.getLong(1);
                result.add(new ACLAssignment(rs.getLong(2), groupId, rs.getBoolean(3), rs.getBoolean(4),
                        rs.getBoolean(7), rs.getBoolean(5), rs.getBoolean(6), rs.getBoolean(8),
                        ACLCategory.getById(rs.getByte(9)), LifeCycleInfoImpl.load(rs, 10, 11, 12, 13)));
            }
            return result;
        } catch (SQLException exc) {
            FxLoadException dbe = new FxLoadException(
                    "Failed to load the ACL assignments for user [" + accountId + "]: " + exc.getMessage(), exc);
            LOG.error(dbe);
            throw dbe;
        } finally {
            Database.closeObjects(ACLEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void fixContactData() throws FxApplicationException {
        List<Account> acct = loadAll(null, null, null, null, null, null, null, null, 0, Integer.MAX_VALUE);
        FxContext.get().runAsSystem();
        try {
            for (Account a : acct) {
                if (a.getContactData().getId() == -1) {
                    FxContent contactData = co
                            .initialize(CacheAdmin.getEnvironment().getType(FxType.CONTACTDATA).getId());
                    contactData.setAclId(ACL.ACL_CONTACTDATA);
                    contactData.setValue("/SURNAME", new FxString(false, a.getName()));
                    //contactData.setValue("/DISPLAYNAME", new FxString(true, a.getName()));
                    contactData.setValue("/EMAIL", new FxString(false, a.getEmail()));
                    FxPK contactDataPK = co.save(contactData);
                    update(a.getId(), null, null, null, null, null, null, null, null, null, null, null, null,
                            contactDataPK.getId());
                }
            }
        } finally {
            FxContext.get().stopRunAsSystem();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public String generateRestToken() throws FxApplicationException {
        final UserTicket ticket = FxContext.getUserTicket();

        if (ticket.isGuest()) {
            throw new FxUpdateException("ex.account.rest.generate.guest");
        }

        if (!ticket.isInRole(Role.RestApiAccess)) {
            throw new FxNoAccessException("ex.account.rest.generate.role", ticket.getLoginName(),
                    Role.RestApiAccess.getName());
        }

        Connection con = null;
        PreparedStatement stmt = null;
        final DBStorage storage = StorageManager.getStorageImpl();
        try {
            con = Database.getDbConnection();
            stmt = con.prepareStatement(
                    "UPDATE " + DatabaseConst.TBL_ACCOUNTS + " SET REST_TOKEN=?, REST_EXPIRES=? WHERE ID=?");

            final String token = RandomStringUtils.randomAlphanumeric(REST_TOKEN_LENGTH);
            stmt.setString(1, token);
            stmt.setLong(2, System.currentTimeMillis() + REST_TOKEN_EXPIRY);
            stmt.setLong(3, ticket.getUserId());

            stmt.executeUpdate();

            return token;
        } catch (SQLException e) {
            if (storage.isUniqueConstraintViolation(e)) {
                // try again
                Database.closeObjects(AccountEngineBean.class, con, stmt);
                generateRestToken();
            }
            throw new FxDbException(e);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void loginByRestToken(String token) throws FxApplicationException {
        Connection con = null;
        PreparedStatement stmt = null;

        if (token == null || token.length() != REST_TOKEN_LENGTH) {
            throw new FxInvalidParameterException("token", "ex.account.rest.token.invalid", token);
        }

        try {
            con = Database.getDbConnection();
            stmt = con.prepareStatement(
                    "SELECT LOGIN_NAME, REST_EXPIRES FROM " + DatabaseConst.TBL_ACCOUNTS + " WHERE REST_TOKEN=?");
            stmt.setString(1, token);

            final ResultSet rs = stmt.executeQuery();

            if (rs.next()) {
                final String loginName = rs.getString(1);
                final long expires = rs.getLong(2);
                if (expires < System.currentTimeMillis()) {
                    throw new FxRestApiTokenExpiredException(token);
                }

                // request-only "login" (not tracked, just replace the user ticket)
                FxContext.startRunningAsSystem();
                try {
                    FxContext.get().overrideTicket(UserTicketStore.getUserTicket(loginName));
                } finally {
                    FxContext.stopRunningAsSystem();
                }
            } else {
                // token invalid (or overwritten by a new token)
                throw new FxRestApiTokenExpiredException(token);
            }
        } catch (SQLException e) {
            throw new FxDbException(e);
        } finally {
            Database.closeObjects(AccountEngineBean.class, con, stmt);
        }
    }

    private UserTicket getRequestTicket() {
        final UserTicket requestTicket = FxContext.getUserTicket();
        return requestTicket != null ? requestTicket : UserTicketStore.getTicket(false);
    }

}