/*
 * WebTop Services is a Web Application framework developed by Sonicle S.r.l.
 * Copyright (C) 2014 Sonicle S.r.l.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY SONICLE, SONICLE DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program 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 Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Sonicle S.r.l. at email address sonicle@sonicle.com
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * Sonicle logo and Sonicle copyright notice. If the display of the logo is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Copyright (C) 2014 Sonicle S.r.l.".
 */
package com.sonicle.webtop.core.app;

import com.sonicle.commons.AlgoUtils;
import com.sonicle.commons.EnumUtils;
import com.sonicle.commons.InternetAddressUtils;
import com.sonicle.commons.LangUtils;
import com.sonicle.commons.PathUtils;
import com.sonicle.commons.db.DbUtils;
import com.sonicle.commons.time.DateTimeUtils;
import com.sonicle.security.AuthenticationDomain;
import com.sonicle.security.ConnectionSecurity;
import com.sonicle.security.DomainAccount;
import com.sonicle.security.PasswordUtils;
import com.sonicle.security.Principal;
import com.sonicle.security.auth.DirectoryException;
import com.sonicle.security.auth.DirectoryManager;
import com.sonicle.security.auth.EntryException;
import com.sonicle.security.auth.directory.ADDirectory;
import com.sonicle.security.auth.directory.AbstractDirectory;
import com.sonicle.security.auth.directory.AbstractDirectory.AuthUser;
import com.sonicle.security.auth.directory.DirectoryCapability;
import com.sonicle.security.auth.directory.DirectoryOptions;
import com.sonicle.security.auth.directory.ImapDirectory;
import com.sonicle.security.auth.directory.LdapDirectory;
import com.sonicle.security.auth.directory.LdapNethDirectory;
import com.sonicle.security.auth.directory.SftpDirectory;
import com.sonicle.security.auth.directory.SmbDirectory;
import com.sonicle.webtop.core.CoreServiceSettings;
import com.sonicle.webtop.core.CoreUserSettings;
import com.sonicle.webtop.core.app.auth.LdapWebTopDirectory;
import com.sonicle.webtop.core.app.auth.WebTopDirectory;
import com.sonicle.webtop.core.app.sdk.WTMultiCauseWarnException;
import com.sonicle.webtop.core.bol.AssignedGroup;
import com.sonicle.webtop.core.bol.AssignedRole;
import com.sonicle.webtop.core.bol.AssignedUser;
import com.sonicle.webtop.core.bol.GroupUid;
import com.sonicle.webtop.core.bol.ODomain;
import com.sonicle.webtop.core.bol.OGroup;
import com.sonicle.webtop.core.bol.ORole;
import com.sonicle.webtop.core.bol.ORoleAssociation;
import com.sonicle.webtop.core.bol.ORolePermission;
import com.sonicle.webtop.core.bol.OUser;
import com.sonicle.webtop.core.bol.OUserAssociation;
import com.sonicle.webtop.core.bol.OUserInfo;
import com.sonicle.webtop.core.bol.UserId;
import com.sonicle.webtop.core.bol.UserUid;
import com.sonicle.webtop.core.bol.model.DirectoryUser;
import com.sonicle.webtop.core.bol.model.DomainEntity;
import com.sonicle.webtop.core.bol.model.GroupEntity;
import com.sonicle.webtop.core.bol.model.ParamsLdapDirectory;
import com.sonicle.webtop.core.bol.model.Role;
import com.sonicle.webtop.core.bol.model.RoleEntity;
import com.sonicle.webtop.core.bol.model.RoleWithSource;
import com.sonicle.webtop.core.model.ServicePermission;
import com.sonicle.webtop.core.bol.model.UserEntity;
import com.sonicle.webtop.core.dal.ActivityDAO;
import com.sonicle.webtop.core.dal.AutosaveDAO;
import com.sonicle.webtop.core.dal.CausalDAO;
import com.sonicle.webtop.core.dal.DAOException;
import com.sonicle.webtop.core.dal.DomainDAO;
import com.sonicle.webtop.core.dal.DomainSettingDAO;
import com.sonicle.webtop.core.dal.GroupDAO;
import com.sonicle.webtop.core.dal.MessageQueueDAO;
import com.sonicle.webtop.core.dal.RoleAssociationDAO;
import com.sonicle.webtop.core.dal.RoleDAO;
import com.sonicle.webtop.core.dal.RolePermissionDAO;
import com.sonicle.webtop.core.dal.ServiceStoreEntryDAO;
import com.sonicle.webtop.core.dal.ShareDAO;
import com.sonicle.webtop.core.dal.ShareDataDAO;
import com.sonicle.webtop.core.dal.SnoozedReminderDAO;
import com.sonicle.webtop.core.dal.SysLogDAO;
import com.sonicle.webtop.core.dal.UserAssociationDAO;
import com.sonicle.webtop.core.dal.UserDAO;
import com.sonicle.webtop.core.dal.UserInfoDAO;
import com.sonicle.webtop.core.dal.UserSettingDAO;
import com.sonicle.webtop.core.model.PublicImage;
import com.sonicle.webtop.core.sdk.AuthException;
import com.sonicle.webtop.core.sdk.BaseServiceSettings;
import com.sonicle.webtop.core.sdk.UserProfile;
import com.sonicle.webtop.core.sdk.UserProfileId;
import com.sonicle.webtop.core.sdk.WTCyrusException;
import com.sonicle.webtop.core.sdk.WTException;
import com.sonicle.webtop.core.sdk.WTRuntimeException;
import com.sonicle.webtop.core.util.IdentifierUtils;
import com.sun.mail.imap.ACL;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.Rights;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import javax.mail.Folder;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.internet.InternetAddress;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

/**
 *
 * @author malbinola
 */
public final class WebTopManager {
	private static final Logger logger = WT.getLogger(WebTopManager.class);
	private static boolean initialized = false;
	
	/**
	 * Initialization method. This method should be called once.
	 * 
	 * @param wta WebTopApp instance.
	 * @return The instance.
	 */
	static synchronized WebTopManager initialize(WebTopApp wta) {
		if(initialized) throw new RuntimeException("Initialization already done");
		WebTopManager instance = new WebTopManager(wta);
		initialized = true;
		logger.info("Initialized");
		return instance;
	}
	
	private WebTopApp wta = null;
	public static final String INTERNETNAME_LOCAL = "local";
	public static final String ROLEUID_SYSADMIN = "SYSADMIN";
	public static final String ROLEUID_WTADMIN = "WTADMIN";
	public static final String ROLEUID_IMPERSONATED_USER = "IMPERSONATED_USER";
	//public static final String SYSADMIN_PSTRING = ServicePermission.permissionString(ServicePermission.namespacedName(CoreManifest.ID, "SYSADMIN"), "ACCESS", "*");
	//public static final String WTADMIN_PSTRING = ServicePermission.permissionString(ServicePermission.namespacedName(CoreManifest.ID, "WTADMIN"), "ACCESS", "*");
	public static final String SYSADMIN_DOMAINID = "*";
	public static final String SYSADMIN_USERID = "admin";
	public static final String DOMAINADMIN_USERID = "admin";
	public static final String GROUPID_ADMINS = "admins";
	public static final String GROUPID_USERS = "users";
	public static final String GROUPID_PEC_ACCOUNTS = "pec-accounts";
	
	private final Object lock0 = new Object();
	private final HashMap<String, String> cachePublicNameToDomain = new HashMap<>();
	private final HashMap<String, String> cacheInternetNameToDomain = new HashMap<>();
	
	private final Object lock1 = new Object();
	private final HashMap<UserProfileId, String> cacheUserToUserUid = new HashMap<>();
	private final HashMap<String, UserProfileId> cacheUserUidToUser = new HashMap<>();
	private final Object lock2 = new Object();
	private final HashMap<UserProfileId, String> cacheGroupToGroupUid = new HashMap<>();
	private final HashMap<String, UserProfileId> cacheGroupUidToGroup = new HashMap<>();
	
	private final HashMap<UserProfileId, UserProfile.PersonalInfo> cacheUserToPersonalInfo = new HashMap<>();
	private final HashMap<UserProfileId, UserProfile.Data> cacheUserToData = new HashMap<>();
	private final Object lock3 = new Object();
	
	/**
	 * Private constructor.
	 * Instances of this class must be created using static initialize method.
	 * @param wta WebTopApp instance.
	 */
	private WebTopManager(WebTopApp wta) {
		this.wta = wta;
		initDomainCache();
		initGroupUidCache();
		initUserUidCache();
	}
	
	/**
	 * Performs cleanup process.
	 */
	void cleanup() {
		cleanupUserUidCache();
		cleanupGroupUidCache();
		cleanupUserCache();
		cleanupDomainCache();
		wta = null;
		logger.info("Cleaned up");
	}
	
	public void cleanUserProfileCache(UserProfileId pid) {
		removeFromUserCache(pid);
	}
	
	public static String generateSecretKey() {
		return StringUtils.defaultIfBlank(IdentifierUtils.generateSecretKey(), "0123456789101112");
	}
	
	public String createSysAdminAuthDirectoryUri() throws URISyntaxException {
		DirectoryManager dirManager = DirectoryManager.getManager();
		return dirManager.getDirectory(WebTopDirectory.SCHEME).buildUri("localhost", null, null).toString();
	}
	
	public AbstractDirectory getAuthDirectory(String authUri) throws WTException {
		try {
			return getAuthDirectoryByScheme(new URI(authUri).getScheme());
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid authentication URI [{0}]", authUri);
		}
	}
	
	public AbstractDirectory getAuthDirectory(URI authUri) throws WTException {
		return getAuthDirectoryByScheme(authUri.getScheme());
	}
	
	public AbstractDirectory getAuthDirectoryByScheme(String scheme) throws WTException {
		DirectoryManager dirManager = DirectoryManager.getManager();
		AbstractDirectory directory = dirManager.getDirectory(scheme);
		if(directory == null) throw new WTException("Directory not supported [{0}]", scheme);
		return directory;
	}
	
	public AuthenticationDomain createAuthenticationDomain(ODomain domain) throws URISyntaxException {
		return new AuthenticationDomain(domain.getDomainId(), 
				domain.getInternetName(), 
				domain.getDirUri(), 
				domain.getDirCaseSensitive(),
				domain.getDirAdmin(), 
				(domain.getDirPassword() != null) ? domain.getDirPassword().toCharArray() : null, 
				EnumUtils.getEnum(ConnectionSecurity.class, domain.getDirConnectionSecurity()),
				domain.getDirParameters()
		);
	}
	
	public AuthenticationDomain createSysAdminAuthenticationDomain() throws URISyntaxException {
		return new AuthenticationDomain("*", null, createSysAdminAuthDirectoryUri(), false, null, null, null, null);
	}
	
	public String domainIdToPublicName(String domainId) {
		return AlgoUtils.adler32Hex(domainId);
	}
	
	public String publicNameToDomainId(String domainPublicName) {
		synchronized(lock0) {
			return cachePublicNameToDomain.get(domainPublicName);
		}
	}
	
	public String internetNameToDomain(String internetName) {
		synchronized (lock0) {
			if (cacheInternetNameToDomain.size() == 1) {
				// If we have only one domain in cache, simply returns it...
				Map.Entry<String, String> entry = cacheInternetNameToDomain.entrySet().iterator().next();
				return entry.getValue();
			} else {
				for(int i=2; i<255; i++) {
					final int iOfNDot = StringUtils.lastOrdinalIndexOf(internetName, ".", i);
					final String key = StringUtils.substring(internetName, iOfNDot+1);
					if(cacheInternetNameToDomain.containsKey(key)) {
						return cacheInternetNameToDomain.get(key);
					}
				}
				return null;
			}
			//return cacheInternetNameToDomain.get(internetName);
		}
	}
	
	public void checkDomains() {
		DomainDAO dao = DomainDAO.getInstance();
		Connection con = null;
		boolean needsCacheReload = false;
		
		try {
			logger.debug("Checking domains...");
			con = wta.getConnectionManager().getConnection();
			for (ODomain odomain : dao.selectAll(con)) {
				try {
					if (doPrepareDomain(con, odomain)) needsCacheReload = true;
				} catch(WTException ex1) {
					logger.error("Unable to verify domain [{}]", odomain.getDomainId(), ex1);
				}
			}
			
		} catch(SQLException ex) {
			throw new WTRuntimeException(ex, "Unable to verify domains");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		if (needsCacheReload) {
			initGroupUidCache();
			initUserUidCache();
		}
	}
	
	public List<ODomain> listDomains(boolean enabledOnly) throws WTException {
		DomainDAO dao = DomainDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			if(enabledOnly) {
				return dao.selectEnabled(con);
			} else {
				return dao.selectAll(con);
			}
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public List<ODomain> listByInternetDomain(String internetDomain) throws WTException {
		DomainDAO dao = DomainDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return dao.selectEnabledByInternetName(con, internetDomain);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public ODomain getDomain(String domainId) throws WTException {
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return getDomain(con, domainId);
			
		} catch(SQLException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public Boolean getDomainDirPasswordPolicy(String domainId) throws WTException {
		DomainDAO dao = DomainDAO.getInstance();
		Connection con = null;
		
		try {
			if (domainId.equals("*")) return false;
			con = wta.getConnectionManager().getConnection();
			return dao.selectDirPasswordPolicyById(con, domainId);
			
		} catch(SQLException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private ODomain getDomain(Connection con, String domainId) throws WTException {
		DomainDAO dao = DomainDAO.getInstance();
		
		try {
			return dao.selectById(con, domainId);
			
		} catch(DAOException ex) {
			throw new WTException(ex, "DB error");
		}
	}
	
	public DomainEntity getDomainEntity(String domainId) throws WTException {
		
		ODomain domain = getDomain(domainId);
		try {
			DomainEntity de = new DomainEntity(domain);
			de.setDirPassword(getDirPassword(domain));
			return de;
			
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid directory URI");
		}
	}
	
	public void initDomainWithDefaults(String domainId) throws WTException {
		ODomain odom = getDomain(domainId);
		if (odom == null) throw new WTException("Domain not found [{}]", domainId);
		
		Connection con = null;
		try {
			con = wta.getConnectionManager().getConnection();
			doPrepareDomain(con, odom);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private boolean doPrepareDomain(Connection con, ODomain domain) throws WTException {
		GroupDAO grpDao = GroupDAO.getInstance();
		UserDAO usrDao = UserDAO.getInstance();
		boolean changed = false;
		
		final List<String> BUILT_IN_GROUPS = Arrays.asList(GROUPID_ADMINS, GROUPID_USERS, GROUPID_PEC_ACCOUNTS);
		Map<String, OGroup> groups = grpDao.selectByDomainIn(con, domain.getDomainId(), BUILT_IN_GROUPS);
		
		// Prepare built-in groups
		logger.debug("Checking built-in groups... [{}]", domain.getDomainId());
		if (!groups.containsKey(GROUPID_ADMINS)) {
			OGroup ogroup = doGroupInsert(con, domain.getDomainId(), GROUPID_ADMINS, "Admins");
			addToGroupUidCache(new GroupUid(ogroup.getDomainId(), ogroup.getUserId(), ogroup.getUserUid()));
			changed = true;
		}
		if (!groups.containsKey(GROUPID_USERS)) {
			OGroup ogroup = doGroupInsert(con, domain.getDomainId(), GROUPID_USERS, "Users");
			addToGroupUidCache(new GroupUid(ogroup.getDomainId(), ogroup.getUserId(), ogroup.getUserUid()));
			changed = true;
		}
		if (!groups.containsKey(GROUPID_PEC_ACCOUNTS)) {
			OGroup ogroup = doGroupInsert(con, domain.getDomainId(), GROUPID_PEC_ACCOUNTS, "PEC Accounts");
			addToGroupUidCache(new GroupUid(ogroup.getDomainId(), ogroup.getUserId(), ogroup.getUserUid()));
			changed = true;
		}
		
		// Prepare built-in admin(for domain) user
		logger.debug("Checking built-in domain admin... [{}]", domain.getDomainId());
		if (!usrDao.existByDomainUser(con, domain.getDomainId(), DOMAINADMIN_USERID)) {
			UserEntity ue = new UserEntity();
			ue.setDomainId(domain.getDomainId());
			ue.setUserId(DOMAINADMIN_USERID);
			ue.setEnabled(true);
			ue.setFirstName("DomainAdmin");
			ue.setLastName(domain.getDescription());
			ue.setDisplayName(ue.getFirstName() + " [" + domain.getDescription() + "]");
			ue.getAssignedGroups().add(new AssignedGroup(WebTopManager.GROUPID_ADMINS));
			addUser(true, ue, true, null);
			changed = true;
		}
		return changed;
	}
	
	public void initDomainHomeFolder(String domainId) throws SecurityException {
		synchronized(lock3) {
			// Main folder (/domains/{domainId})
			File domainDir = new File(wta.getHomePath(domainId));
			if (!domainDir.exists()) domainDir.mkdir();
			
			// Internal folders...
			File tempDir = new File(wta.getTempPath(domainId));
			if (!tempDir.exists()) tempDir.mkdir();
			
			File imagesDir = new File(wta.getImagesPath(domainId));
			if (!imagesDir.exists()) imagesDir.mkdir();
			
			for (String sid : wta.getServiceManager().listRegisteredServices()) {
				File svcDir = new File(wta.getServiceHomePath(domainId, sid));
				if (!svcDir.exists()) svcDir.mkdir();
			}
		}	
	}
	
	public ODomain addDomain(DomainEntity domain) throws WTException {
		DomainDAO dodao = DomainDAO.getInstance();
		Connection con = null;
		ODomain odomain = null;
		
		try {
			con = wta.getConnectionManager().getConnection(false);
			
			logger.debug("Inserting domain");
			odomain = new ODomain();
			fillDomain(odomain, domain);
			dodao.insert(con, odomain);
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		// Update cache
		initDomainCache();
		
		return odomain;
	}
	
	public void updateDomain(DomainEntity domain) throws WTException {
		DomainDAO dodao = DomainDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection(false);
			
			logger.debug("Updating domain");
			ODomain odomain = new ODomain();
			fillDomain(odomain, domain);
			dodao.update(con, odomain);
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		// Update cache
		initDomainCache();
	}
	
	public void deleteDomain(String domainId) throws WTException {
		DomainDAO domdao = DomainDAO.getInstance();
		Connection con = null;
		ODomain odomain = null;
		List<OUser> ousers = null;
		
		try {
			con = wta.getConnectionManager().getConnection(false);
			
			odomain = domdao.selectById(con, domainId);
			if(odomain == null) throw new WTException("Domain not found [{0}]", odomain);
			ousers = listUsers(domainId, false);
			
			logger.debug("Deleting domain");
			ActivityDAO.getInstance().deleteByDomain(con, domainId);
			CausalDAO.getInstance().deleteByDomain(con, domainId);
			
			AutosaveDAO.getInstance().deleteByDomain(con, domainId);
			ServiceStoreEntryDAO.getInstance().deleteByDomain(con, domainId);
			SnoozedReminderDAO.getInstance().deleteByDomain(con, domainId);
			MessageQueueDAO.getInstance().deleteByDomain(con, domainId);
			SysLogDAO.getInstance().deleteByDomain(con, domainId);
			
			DomainSettingDAO.getInstance().deleteByDomain(con, domainId);
			UserSettingDAO.getInstance().deleteByDomain(con, domainId);
			
			RoleAssociationDAO.getInstance().deleteByDomain(con, domainId);
			RolePermissionDAO.getInstance().deleteByDomain(con, domainId);
			RoleDAO.getInstance().deleteByDomain(con, domainId);
			ShareDAO.getInstance().deleteByDomain(con, domainId);
			ShareDataDAO.getInstance().deleteByDomain(con, domainId);
			
			UserAssociationDAO.getInstance().deleteByDomain(con, domainId);
			UserInfoDAO.getInstance().deleteByDomain(con, domainId);
			UserDAO.getInstance().deleteByDomain(con, domainId);
			GroupDAO.getInstance().deleteByDomain(con, domainId);
			domdao.deleteById(con, domainId);
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		// Update cache
		initDomainCache();
		initUserUidCache();
		initGroupUidCache();
		cleanupUserCache();
		
		try {
			AuthenticationDomain ad = createAuthenticationDomain(odomain);
			AbstractDirectory directory = getAuthDirectory(ad.getDirUri());
			DirectoryOptions opts = wta.createDirectoryOptions(ad);
			
			if(directory.hasCapability(DirectoryCapability.USERS_WRITE)) {
				for(OUser ouser : ousers) {
					final UserProfileId pid = new UserProfileId(ouser.getDomainId(), ouser.getUserId());
					directory.deleteUser(opts, pid.getDomainId(), pid.getUserId());
				}
			}
			
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid domain auth URI");
		} catch(DirectoryException ex) {
			throw new WTException(ex, "DirectoryException error");
		}

		//TODO: chiamare controller per eliminare dominio per i servizi
	}
	
	public List<PublicImage> listDomainPublicImages(String domainId) throws WTException {
		String path = WT.getDomainImagesPath(domainId);
		String baseUrl = WT.getPublicImagesUrl(domainId);
		File dir = new File(path);
		ArrayList<PublicImage> items = new ArrayList<>();
		for(File file : dir.listFiles()) {
			String name = file.getName();
			String url = PathUtils.concatPathParts(baseUrl, name);
			items.add(new PublicImage(name, url));
		}
		return items;
	}
	
	public List<OUser> listUsers(String domainId, boolean enabledOnly) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			if(enabledOnly) {
				return dao.selectEnabledByDomain(con, domainId);
			} else {
				return dao.selectByDomain(con, domainId);
			}
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public List<UserProfileId> listUserProfileIdsByEmail(String emailAddress) throws WTException {
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			
			List<UserId> uids = uidao.viewByEmail(con, emailAddress);
			ArrayList<UserProfileId> items = new ArrayList<>();
			for(UserId uid : uids) {
				items.add(new UserProfileId(uid.getDomainId(), uid.getUserId()));
			}
			return items;
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public OUser getUser(UserProfileId pid) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return dao.selectByDomainUser(con, pid.getDomainId(), pid.getUserId());
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public UserEntity getUserEntity(UserProfileId pid) throws WTException {
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return getUserEntity(con, pid);
			
		} catch(SQLException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private UserEntity getUserEntity(Connection con, UserProfileId pid) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		UserAssociationDAO uassdao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		
		try {
			OUser ouser = dao.selectByDomainUser(con, pid.getDomainId(), pid.getUserId());
			if(ouser == null) throw new WTException("User not found [{0}]", pid.toString());
			OUserInfo ouseri = uidao.selectByDomainUser(con, pid.getDomainId(), pid.getUserId());
			if(ouseri == null) throw new WTException("User info not found [{0}]", pid.toString());
			
			List<AssignedGroup> assiGroups = uassdao.viewAssignedByUser(con, ouser.getUserUid());
			List<AssignedRole> assiRoles = rolassdao.viewAssignedByUser(con, ouser.getUserUid());
			EntityPermissions perms = extractPermissions(con, ouser.getUserUid());
			
			UserEntity user = new UserEntity(ouser, ouseri);
			user.setAssignedGroups(assiGroups);
			user.setAssignedRoles(assiRoles);
			user.setPermissions(perms.others);
			user.setServicesPermissions(perms.services);
			
			return user;
			
		} catch(DAOException ex) {
			throw new WTException(ex, "DB error");
		}
	}
	
	public void addUser(boolean updateDirectory, UserEntity user, char[] password) throws WTException {
		addUser(updateDirectory, user, true, password);
	}
	
	public void addUser(boolean updateDirectory, UserEntity user, boolean updatePassword, char[] password) throws WTException {
		UserProfileId addedPid = null;
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection(false);
			
			ODomain domain = getDomain(user.getDomainId());
			if (domain == null) throw new WTException("Domain not found [{}]", user.getDomainId());
			AuthenticationDomain ad = createAuthenticationDomain(domain);
			
			user.ensureCoherence();
			user.getAssignedGroups().add(new AssignedGroup(WebTopManager.GROUPID_USERS));
			
			OUser ouser = null;
			if (updateDirectory) {
				AbstractDirectory authDir = getAuthDirectory(ad.getDirUri());
				DirectoryOptions opts = wta.createDirectoryOptions(ad);
				
				if (authDir.hasCapability(DirectoryCapability.USERS_WRITE)) {
					if (!authDir.validateUsername(opts, user.getUserId())) {
						throw new WTException("Username does not satisfy directory requirements [{}]", ad.getDirUri().getScheme());
					}
				}
				if (updatePassword && authDir.hasCapability(DirectoryCapability.PASSWORD_WRITE)) {
					if (password == null) {
						password = authDir.generatePassword(opts, domain.getDirPasswordPolicy());
					} else {
						if (domain.getDirPasswordPolicy() && !authDir.validatePasswordPolicy(opts, password)) {
							throw new WTException("Password does not satisfy directory password policy [{}]", ad.getDirUri().getScheme());
						}
					}
				}
				
				ouser = doUserInsert(con, domain, user);
				addedPid = new UserProfileId(ouser.getDomainId(), ouser.getUserId());
				
				// Insert user in directory (if necessary)
				if (authDir.hasCapability(DirectoryCapability.USERS_WRITE)) {
					logger.debug("Adding user into directory...");
					try {
						authDir.addUser(opts, domain.getDomainId(), createAuthUser(user));
					} catch(EntryException ex1) {
						logger.warn("Insertion skipped: user already exists [{}]", user.getUserId());
					}
				}
				if (updatePassword && authDir.hasCapability(DirectoryCapability.PASSWORD_WRITE)) {
					logger.debug("Updating its password");
					authDir.updateUserPassword(opts, addedPid.getDomainId(), addedPid.getUserId(), password);
					new CoreUserSettings(wta.getSettingsManager(), addedPid).setPasswordLastChange(DateTimeUtils.now());
				}
				
			} else {
				ouser = doUserInsert(con, domain, user);
				addedPid = new UserProfileId(ouser.getDomainId(), ouser.getUserId());
			}
			
			DbUtils.commitQuietly(con);
			
			// Update cache
			addToUserUidCache(new UserUid(addedPid.getDomainId(), addedPid.getUserId(), ouser.getUserUid()));

			// Explicitly sets some important (locale & timezone) user settings to their defaults
			CoreServiceSettings css = new CoreServiceSettings(wta.getSettingsManager(), CoreManifest.ID, addedPid.getDomainId());
			CoreUserSettings cus = new CoreUserSettings(wta.getSettingsManager(), addedPid);
			cus.setLanguageTag(css.getDefaultLanguageTag());
			cus.setTimezone(css.getDefaultTimezone());
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid domain auth URI");
		} catch(DirectoryException ex) {
			throw new WTException(ex, "DirectoryException error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		// Performs some actions after the add operation
		if (addedPid != null) {
			List<Throwable> errors = wta.getServiceManager().invokeOnUserAdded(addedPid);
			if (!errors.isEmpty()) throw new WTMultiCauseWarnException(errors, "Errors in user related listeners");
		}		
	}
	
	public CheckUserResult checkUser(UserProfileId pid) throws WTException {
		return checkUser(pid.getDomainId(), pid.getUserId());
	}
	
	public CheckUserResult checkUser(String domainId, String userId) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			OUser o = dao.selectByDomainUser(con, domainId, userId);
			return new CheckUserResult(o != null, o != null ? o.getEnabled() : false);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void updateUser(UserEntity user) throws WTException {
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			user.ensureCoherence();
			doUserUpdate(con, user);
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public boolean updateUser(UserProfileId pid, boolean enabled) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return dao.updateEnabledByDomainUser(con, pid.getDomainId(), pid.getUserId(), enabled) == 1;
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public int updateUserEmailDomain(List<UserProfileId> pids, String newEmailDomain) throws WTException {
		UserInfoDAO uiDao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			if (pids.isEmpty()) return -1;
			con = wta.getConnectionManager().getConnection(false);
			
			String domainId = pids.get(0).getDomainId();
			List<String> userIds = pids.stream().map(pid -> pid.getUserId()).collect(Collectors.toList());
			String emailDomain = "@" + StringUtils.removeStart(newEmailDomain, "@");
			int ret = uiDao.updateEmailDomainByProfiles(con, domainId, userIds, emailDomain);
			DbUtils.commitQuietly(con);
			
			// Clean-up cache!
			for (UserProfileId pid : pids) cleanUserProfileCache(pid);
			
			return ret;
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw wrapThrowable(ex);
		} catch(Throwable t) {
			DbUtils.rollbackQuietly(con);
			throw t;
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private AuthenticationDomain createAuthenticationDomain(UserProfileId profileId) throws WTException {
		try {
			if (Principal.xisAdmin(profileId.getDomainId(), profileId.getUserId())) {
				return createSysAdminAuthenticationDomain();

			} else {
				ODomain odom = getDomain(profileId.getDomainId());
				if (odom == null) throw new WTException("Domain not found [{0}]", profileId.getDomainId());
				return createAuthenticationDomain(odom);
			}
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid URI");
		}
	}
	
	public void updateUserPassword(UserProfileId profileId, char[] oldPassword, char[] newPassword) throws WTException, EntryException {
		AuthenticationDomain ad = createAuthenticationDomain(profileId);
		
		try {
			AbstractDirectory directory = getAuthDirectory(ad.getDirUri());
			DirectoryOptions opts = wta.createDirectoryOptions(ad);
			
			if (!directory.hasCapability(DirectoryCapability.PASSWORD_WRITE)) {
				throw new WTException("Directory has no write capability");
			}
			Boolean passwordPolicy = getDomainDirPasswordPolicy(profileId.getDomainId());
			if (passwordPolicy && !directory.validatePasswordPolicy(opts, newPassword)) {
				throw new WTException("Provided password does not satisfy directory password policy");
			}
			if (oldPassword != null) {
				directory.updateUserPassword(opts, profileId.getDomainId(), profileId.getUserId(), oldPassword, newPassword);
			} else {
				directory.updateUserPassword(opts, profileId.getDomainId(), profileId.getUserId(), newPassword);
			}

			CoreUserSettings cus = new CoreUserSettings(profileId);
			cus.setPasswordLastChange(DateTimeUtils.now());
			cus.setPasswordForceChange(false);
			
		} catch(DirectoryException ex) {
			throw new WTException(ex, "Directory error");
		}
	}
	
	public boolean isUserPasswordChangeNeeded(UserProfileId profileId, char[] password) throws WTException {
		if (Principal.xisAdmin(profileId.getDomainId(), profileId.getUserId())) return false;
		
		AuthenticationDomain ad = createAuthenticationDomain(profileId);
		AbstractDirectory directory = getAuthDirectory(ad.getDirUri());
		if (!directory.hasCapability(DirectoryCapability.PASSWORD_WRITE)) return false;
		
		Boolean passwordPolicy = getDomainDirPasswordPolicy(profileId.getDomainId());
		CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, profileId.getDomainId());
		if (passwordPolicy && css.getPasswordForceChangeIfPolicyUnmet()) {
			DirectoryOptions opts = wta.createDirectoryOptions(ad);
			if (!directory.validatePasswordPolicy(opts, password)) return true;
		}
		
		CoreUserSettings cus = new CoreUserSettings(profileId);
		if (cus.getPasswordForceChange()) return true;
		
		return false;
	}
	
	public void deleteUser(UserProfileId pid, boolean cleanupDirectory) throws WTException {
		UserDAO udao = UserDAO.getInstance();
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		UserAssociationDAO uadao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		RolePermissionDAO rpdao = RolePermissionDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			OUser user = udao.selectByDomainUser(con, pid.getDomainId(), pid.getUserId());
			if (user == null) throw new WTException("User not found [{0}]", pid.toString());
			
			logger.debug("Deleting permissions [{}]", user.getUserUid());
			rpdao.deleteByRole(con, user.getUserUid());
			logger.debug("Deleting groups associations [{}]", user.getUserUid());
			uadao.deleteByUser(con, user.getUserUid());
			logger.debug("Deleting roles associations [{}]", user.getUserUid());
			rolassdao.deleteByUser(con, user.getUserUid());
			logger.debug("Deleting userInfo [{}]", pid.toString());
			uidao.deleteByDomainUser(con, pid.getDomainId(), pid.getUserId());
			logger.debug("Deleting user [{}]", pid.toString());
			udao.deleteByDomainUser(con, pid.getDomainId(), pid.getUserId());
			
			if (cleanupDirectory) {
				ODomain domain = getDomain(pid.getDomainId());
				if (domain == null) throw new WTException("Domain not found [{}]", pid.getDomainId());
				
				AuthenticationDomain ad = createAuthenticationDomain(domain);
				AbstractDirectory directory = getAuthDirectory(ad.getDirUri());
				DirectoryOptions opts = wta.createDirectoryOptions(ad);
				
				if (directory.hasCapability(DirectoryCapability.USERS_WRITE)) {
					directory.deleteUser(opts, pid.getDomainId(), pid.getUserId());
				}
			}
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} catch(URISyntaxException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "Invalid URI");
		} catch(DirectoryException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "Directory error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		
		// Performs some actions after the remove operation
		List<Throwable> errors = wta.getServiceManager().invokeOnUserRemoved(pid);
		wta.getSettingsManager().clearUserSettings(pid.getDomainId(), pid.getUserId());
		
		// Update cache
		removeFromUserUidCache(pid);
		removeFromUserCache(pid);
		
		if (!errors.isEmpty()) throw new WTMultiCauseWarnException(errors, "Errors in user related listeners");
	}
	
	/**
	 * Returns the internal UID associated to specified group; 
	 * usually applied to defaults/built-in groups.
	 * @param domainId The domain ID.
	 * @param groupId The group ID.
	 * @return The group's UID if found; null otherwise
	 */
	public String getGroupUid(String domainId, String groupId) {
		UserProfileId pid = new UserProfileId(domainId, groupId);
		try {
			return groupToUid(pid);
		} catch(WTRuntimeException ex) {
			logger.debug("Unable to get group's UID [{}]", pid.toString(), ex);
			return null;
		}
	}
	
	/**
	 * Returns the internal UID associated to specified user.
	 * @param domainId The domain ID.
	 * @param userId The user ID.
	 * @return The user's UID if found; null otherwise
	 */
	public String getUserUid(String domainId, String userId) {
		UserProfileId pid = new UserProfileId(domainId, userId);
		try {
			return userToUid(pid);
		} catch(WTRuntimeException ex) {
			logger.debug("Unable to get user's UID [{}]", pid.toString(), ex);
			return null;
		}
	}
	
	public List<DirectoryUser> listDirectoryUsers(ODomain domain) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			AuthenticationDomain ad = createAuthenticationDomain(domain);
			AbstractDirectory directory = getAuthDirectory(ad.getDirUri());
			DirectoryOptions opts = wta.createDirectoryOptions(ad);
			
			con = wta.getConnectionManager().getConnection();
			Map<String, OUser> wtUsers = dao.selectByDomain2(con, domain.getDomainId());
			
			ArrayList<DirectoryUser> items = new ArrayList<>();
			
			if(directory.hasCapability(DirectoryCapability.USERS_READ)) {
				for(AuthUser userEntry : directory.listUsers(opts, domain.getDomainId())) {
					items.add(new DirectoryUser(domain.getDomainId(), userEntry, wtUsers.get(userEntry.userId)));
				}
				
			} else {
				for(OUser ouser : wtUsers.values()) {
					final AbstractDirectory.AuthUser userEntry = new AbstractDirectory.AuthUser(ouser.getUserId(), ouser.getDisplayName(), null, null, null);
					items.add(new DirectoryUser(domain.getDomainId(), userEntry, ouser));
				}
			}
			
			return items;
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} catch(URISyntaxException ex) {
			throw new WTException(ex, "Invalid URI");
		} catch(DirectoryException ex) {
			throw new WTException(ex, "Directory error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public boolean updateUserDisplayName(UserProfileId pid, String displayName) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return dao.updateDisplayNameByDomainUser(con, pid.getDomainId(), pid.getUserId(), displayName) == 1;
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public UserProfile.PersonalInfo getUserPersonalInfo(UserProfileId pid) throws WTException {
		UserInfoDAO dao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			OUserInfo oui = dao.selectByDomainUser(con, pid.getDomainId(), pid.getUserId());
			return (oui == null) ? null : new UserProfile.PersonalInfo(oui);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public boolean updateUserPersonalInfo(UserProfileId pid, UserProfile.PersonalInfo userPersonalInfo) throws WTException {
		UserInfoDAO dao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			OUserInfo oui = createUserInfo(userPersonalInfo);
			oui.setDomainId(pid.getDomainId());
			oui.setUserId(pid.getUserId());
			return dao.update(con, oui) == 1;
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public List<OGroup> listGroups(String domainId) throws WTException {
		GroupDAO dao = GroupDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return dao.selectByDomain(con, domainId);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public GroupEntity getGroupEntity(UserProfileId pid) throws WTException {
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			return getGroupEntity(con, pid);
			
		} catch(SQLException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private GroupEntity getGroupEntity(Connection con, UserProfileId pid) throws WTException {
		GroupDAO dao = GroupDAO.getInstance();
		UserAssociationDAO uassdao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		
		try {
			OGroup ogroup = dao.selectByDomainGroup(con, pid.getDomainId(), pid.getUserId());
			if(ogroup == null) throw new WTException("Group not found [{0}]", pid.toString());
			
			List<AssignedUser> assiUsers = uassdao.viewAssignedByGroup(con, ogroup.getGroupUid());
			List<AssignedRole> assiRoles = rolassdao.viewAssignedByGroup(con, ogroup.getGroupUid());
			EntityPermissions perms = extractPermissions(con, ogroup.getGroupUid());
			
			GroupEntity group = new GroupEntity(ogroup);
			group.setAssignedUsers(assiUsers);
			group.setAssignedRoles(assiRoles);
			group.setPermissions(perms.others);
			group.setServicesPermissions(perms.services);
			
			return group;
			
		} catch(DAOException ex) {
			throw new WTException(ex, "DB error");
		}
	}
	
	public void addGroup(GroupEntity group) throws WTException {
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection(false);
			
			ODomain domain = getDomain(group.getDomainId());
			if(domain == null) throw new WTException("Domain not found [{0}]", group.getDomainId());
			
			OGroup ogroup = doGroupInsert(con, domain.getDomainId(), group);
			
			DbUtils.commitQuietly(con);
			
			// Update cache
			addToGroupUidCache(new GroupUid(ogroup.getDomainId(), group.getGroupId(), ogroup.getGroupUid()));
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void updateGroup(GroupEntity group) throws WTException {
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			doGroupUpdate(con, group);
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void deleteGroup(UserProfileId pid) throws WTException {
		GroupDAO udao = GroupDAO.getInstance();
		UserAssociationDAO uadao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		RolePermissionDAO rpdao = RolePermissionDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			OGroup group = udao.selectByDomainGroup(con, pid.getDomainId(), pid.getUserId());
			if(group == null) throw new WTException("Group not found [{0}]", pid.toString());
			
			logger.debug("Deleting permissions");
			rpdao.deleteByRole(con, group.getGroupUid());
			logger.debug("Deleting groups associations");
			uadao.deleteByGroup(con, group.getGroupUid());
			logger.debug("Deleting roles associations");
			rolassdao.deleteByUser(con, group.getGroupUid());
			logger.debug("Deleting group");
			udao.deleteByDomainGroup(con, pid.getDomainId(), pid.getUserId());
			
			DbUtils.commitQuietly(con);
			
			// Update cache
			removeFromGroupUidCache(pid);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	/**
	 * Lists domain real roles (those defined as indipendent role).
	 * @param domainId The domain ID.
	 * @return
	 * @throws WTException 
	 */
	public List<Role> listRoles(String domainId) throws WTException {
		RoleDAO dao = RoleDAO.getInstance();
		ArrayList<Role> items = new ArrayList<>();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			
			List<ORole> roles = dao.selectByDomain(con, domainId);
			for(ORole erole : roles) items.add(new Role(erole));
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		return items;
	}
	
	/**
	 * Lists domain users roles (those coming from a user).
	 * @param domainId The domain ID.
	 * @return
	 * @throws WTException 
	 */
	public List<Role> listUsersRoles(String domainId) throws WTException {
		UserDAO dao = UserDAO.getInstance();
		ArrayList<Role> items = new ArrayList<>();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			
			List<OUser> users = dao.selectEnabledByDomain(con, domainId);
			for(OUser user: users) items.add(new Role(user));
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		return items;
	}
	
	/**
	 * Lists domain groups roles (those coming from a group).
	 * @param domainId The domain ID.
	 * @return
	 * @throws WTException 
	 */
	public List<Role> listGroupsRoles(String domainId) throws WTException {
		GroupDAO dao = GroupDAO.getInstance();
		ArrayList<Role> items = new ArrayList<>();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			
			List<OGroup> groups = dao.selectByDomain(con, domainId);
			for(OGroup group: groups) items.add(new Role(group));
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		return items;
	}
	
	public List<AssignedRole> listAssignedRoles(String userUid) throws WTException {
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			return rolassdao.viewAssignedByUser(con, userUid);
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	/**
	 * Retrieves the domain ID for the specified role.
	 * @param uid
	 * @return
	 * @throws WTException 
	 */
	public String getRoleDomain(String uid) throws WTException {
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			
			RoleDAO roldao = RoleDAO.getInstance();
			ORole role = roldao.selectByUid(con, uid);
			if(role != null) return role.getDomainId();
			
			UserDAO usedao = UserDAO.getInstance();
			OUser user = usedao.selectByUid(con, uid);
			if(user != null) return user.getDomainId();
			
			GroupDAO grpdao = GroupDAO.getInstance();
			OGroup group = grpdao.selectByUid(con, uid);
			if(group != null) return group.getDomainId();
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		return null;
	}
	
	public RoleEntity getRole(String uid) throws WTException {
		RoleDAO roldao = RoleDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			
			ORole orole = roldao.selectByUid(con, uid);
			if(orole == null) throw new WTException("Role not found [{0}]", uid);
			
			EntityPermissions perms = extractPermissions(con, uid);
			RoleEntity role = new RoleEntity(orole);
			role.setPermissions(perms.others);
			role.setServicesPermissions(perms.services);
			
			return role;
			
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void addRole(RoleEntity role) throws WTException {
		RoleDAO roldao = RoleDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			ORole orole = new ORole();
			orole.setRoleUid(IdentifierUtils.getUUID());
			orole.setDomainId(role.getDomainId());
			orole.setName(role.getName());
			orole.setDescription(role.getDescription());
			roldao.insert(con, orole);
			
			for(ORolePermission perm : role.getPermissions()) {
				doInsertPermission(con, orole.getRoleUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
			}
			for(ORolePermission perm : role.getServicesPermissions()) {
				doInsertPermission(con, orole.getRoleUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
			}
			
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void updateRole(RoleEntity role) throws WTException {
		RoleDAO roldao = RoleDAO.getInstance();
		RolePermissionDAO rolperdao = RolePermissionDAO.getInstance();
		Connection con = null;
		
		try {
			RoleEntity oldRole = getRole(role.getRoleUid());
			if(oldRole == null) throw new WTException("Role not found [{0}]", role.getRoleUid());
			
			con = WT.getConnection(CoreManifest.ID, false);
			
			ORole orole = new ORole();
			orole.setRoleUid(role.getRoleUid());
			orole.setName(role.getName());
			orole.setDescription(role.getDescription());
			roldao.update(con, orole);
			
			LangUtils.CollectionChangeSet<ORolePermission> changeSet1 = LangUtils.getCollectionChanges(oldRole.getPermissions(), role.getPermissions());
			for(ORolePermission perm : changeSet1.deleted) {
				rolperdao.deleteById(con, perm.getRolePermissionId());
			}
			for(ORolePermission perm : changeSet1.inserted) {
				doInsertPermission(con, oldRole.getRoleUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
			}
			
			LangUtils.CollectionChangeSet<ORolePermission> changeSet2 = LangUtils.getCollectionChanges(oldRole.getServicesPermissions(), role.getServicesPermissions());
			for(ORolePermission perm : changeSet2.deleted) {
				rolperdao.deleteById(con, perm.getRolePermissionId());
			}
			for(ORolePermission perm : changeSet2.inserted) {
				doInsertPermission(con, oldRole.getRoleUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
			}
			
			DbUtils.commitQuietly(con);
		
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void deleteRole(String uid) throws WTException {
		RoleDAO roldao = RoleDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		RolePermissionDAO rolperdao = RolePermissionDAO.getInstance();
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID, false);
			
			roldao.deleteByUid(con, uid);
			rolassdao.deleteByRole(con, uid);
			rolperdao.deleteByRole(con, uid);
			DbUtils.commitQuietly(con);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public List<String> getComputedRolesAsStringByUser(UserProfileId pid, boolean self, boolean transitive) throws WTException {
		ArrayList<String> uids = new ArrayList<>();
		Set<RoleWithSource> roles = getComputedRolesByUser(pid, self, transitive);
		for(RoleWithSource role : roles) {
			uids.add(role.getRoleUid());
		}
		return uids;
	}
	
	public Set<RoleWithSource> getComputedRolesByUser(UserProfileId pid, boolean self, boolean transitive) throws WTException {
		WebTopManager usrm = wta.getWebTopManager();
		Connection con = null;
		HashSet<String> roleMap = new HashSet<>();
		LinkedHashSet<RoleWithSource> roles = new LinkedHashSet<>();
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			String userUid = usrm.userToUid(pid);
			
			if(self) {
				UserDAO usedao = UserDAO.getInstance();
				OUser user = usedao.selectByUid(con, userUid);
				roles.add(new RoleWithSource(RoleWithSource.SOURCE_USER, userUid, user.getDomainId(), pid.getUserId(), user.getDisplayName()));
			}
			
			RoleDAO roldao = RoleDAO.getInstance();
			
			// Gets by group
			List<ORole> groles = roldao.selectFromGroupsByUser(con, userUid);
			for(ORole role : groles) {
				if(roleMap.contains(role.getRoleUid())) continue; // Skip duplicates
				roleMap.add(role.getRoleUid());
				roles.add(new RoleWithSource(RoleWithSource.SOURCE_GROUP, role.getRoleUid(), role.getDomainId(), role.getName(), role.getDescription()));
			}
			
			// Gets direct assigned roles
			List<ORole> droles = roldao.selectDirectByUser(con, userUid);
			for(ORole role : droles) {
				if(roleMap.contains(role.getRoleUid())) continue; // Skip duplicates
				roleMap.add(role.getRoleUid());
				roles.add(new RoleWithSource(RoleWithSource.SOURCE_ROLE, role.getRoleUid(), role.getDomainId(), role.getName(), role.getDescription()));
			}
			
			// Get transivite roles (belonging to groups)
			if(transitive) {
				List<ORole> troles = roldao.selectTransitiveFromGroupsByUser(con, userUid);
				for(ORole role : troles) {
					if(roleMap.contains(role.getRoleUid())) continue; // Skip duplicates
					roleMap.add(role.getRoleUid());
					roles.add(new RoleWithSource(RoleWithSource.SOURCE_TRANSITIVE, role.getRoleUid(), role.getDomainId(), role.getName(), role.getDescription()));
				}
			}
		} catch(SQLException | DAOException ex) {
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
		return roles;
	}
	
	public List<ORolePermission> listRolePermissions(String roleUid) throws Exception {
		Connection con = null;
		
		try {
			con = WT.getConnection(CoreManifest.ID);
			RolePermissionDAO dao = RolePermissionDAO.getInstance();
			return dao.selectByRoleUid(con, roleUid);
		
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public EntityPermissions extractPermissions(Connection con, String roleUid) throws WTException {
		RolePermissionDAO rolperdao = RolePermissionDAO.getInstance();
		
		List<ORolePermission> operms = rolperdao.selectByRoleUid(con, roleUid);
		ArrayList<ORolePermission> othersPerms = new ArrayList<>();
		ArrayList<ORolePermission> servicesPerms = new ArrayList<>();
		for(ORolePermission operm : operms) {
			if(operm.getInstance().equals("*")) {
				othersPerms.add(operm);
			} else {
				if(operm.getServiceId().equals(CoreManifest.ID) && operm.getKey().equals("SERVICE") && operm.getAction().equals("ACCESS")) {
					servicesPerms.add(operm);
				}
			}
		}
		
		return new EntityPermissions(othersPerms, servicesPerms);
	}
	
	public UserProfile.PersonalInfo userPersonalInfo(UserProfileId pid) throws WTException {
		synchronized(cacheUserToPersonalInfo) {
			if(!cacheUserToPersonalInfo.containsKey(pid)) {
				UserProfile.PersonalInfo upi = getUserPersonalInfo(pid);
				if(upi == null) throw new WTException("UserPersonalInfo not found [{0}]", pid.toString());
				cacheUserToPersonalInfo.put(pid, upi);
				return upi;
			} else {
				return cacheUserToPersonalInfo.get(pid);
			}
		}
	}
	
	public UserProfile.Data userData(UserProfileId pid) throws WTException {
		synchronized(cacheUserToData) {
			if(!cacheUserToData.containsKey(pid)) {
				final UserProfile.Data ud = getUserData(pid);
				if (ud == null) return null;
				cacheUserToData.put(pid, ud);
				return ud;
			} else {
				return cacheUserToData.get(pid);
			}
		}
	}
	
	public void ensureProfileDomain(String domainId) throws AuthException {
		if (domainId == null) return;
		UserProfileId runPid = RunContext.getRunProfileId();
		if (RunContext.isWebTopAdmin(runPid)) return;
		if (!runPid.hasDomain(domainId)) throw new AuthException("Running profile's domain [{0}] does not match with passed one [{1}]", runPid.getDomainId(), domainId);
	}
	
	public UserProfileId guessUserProfileIdByProfileUsername(String profileUsername) throws WTException {
		UserProfileId iaPid = null;
		try {
			iaPid = new UserProfileId(profileUsername);
		} catch(UnsupportedOperationException ex) {
			return null;
		}
		
		String domainId = internetNameToDomain(iaPid.getDomain());
		if (domainId == null) return null;
		ensureProfileDomain(domainId);
		return new UserProfileId(domainId, iaPid.getUser());
	}
	
	public UserProfileId guessUserProfileIdByPersonalAddress(String personalAddress) throws WTException {
		UserInfoDAO uiDao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			
			List<UserId> uids = uiDao.viewByEmail(con, personalAddress);
			if (uids.isEmpty()) return null;
			
			UserId uid = uids.get(0);
			ensureProfileDomain(uid.getDomainId());
			return new UserProfileId(uid.getDomainId(), uid.getUserId());
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public UserProfile.Data userDataByEmail(String emailAddress) throws WTException {
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		Connection con = null;
		
		try {
			con = wta.getConnectionManager().getConnection();
			
			List<UserId> uids = uidao.viewByEmail(con, emailAddress);
			if (uids.isEmpty()) return null;
			
			UserId uid = uids.get(0);
			UserProfileId pid = new UserProfileId(uid.getDomainId(), uid.getUserId());
			return userData(pid);
			
		} catch(SQLException | DAOException ex) {
			DbUtils.rollbackQuietly(con);
			throw new WTException(ex, "DB error");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public String userToUid(UserProfileId pid) {
		return userToUid(pid,true);
	}
	
	public String userToUid(UserProfileId pid, boolean mandatory) {
		synchronized(lock1) {
			if(!cacheUserToUserUid.containsKey(pid)) {
				if (mandatory)
					throw new WTRuntimeException("[userToUidCache] Cache miss on key {0}", pid.toString());
				return null;
			}
			return cacheUserToUserUid.get(pid);
		}
	}
	
	public UserProfileId uidToUser(String uid) {
		synchronized(lock1) {
			if(!cacheUserUidToUser.containsKey(uid)) throw new WTRuntimeException("[uidToUserCache] Cache miss on key {0}", uid);
			return cacheUserUidToUser.get(uid);
		}
	}
	
	public String groupToUid(UserProfileId pid) {
		synchronized(lock2) {
			if(!cacheGroupToGroupUid.containsKey(pid)) throw new WTRuntimeException("[groupToUidCache] Cache miss on key {0}", pid.toString());
			return cacheGroupToGroupUid.get(pid);
		}
	}
	
	public UserProfileId uidToGroup(String uid) {
		synchronized(lock2) {
			if(!cacheGroupUidToGroup.containsKey(uid)) throw new WTRuntimeException("[uidToGroupCache] Cache miss on key {0}", uid);
			return cacheGroupUidToGroup.get(uid);
		}
	}
	
	public String getInternetUserId(UserProfileId pid) throws WTException {
		String internetName = getDomainInternetName(pid.getDomainId());
		return new UserProfileId(internetName, pid.getUserId()).toString();
	}
	
	public String getDomainInternetName(String domainId) throws WTException {
		if (StringUtils.equals(domainId, SYSADMIN_DOMAINID)) {
			return INTERNETNAME_LOCAL;
		} else {
			ODomain domain = getDomain(domainId);
			if (domain == null) throw new WTException("Domain not found [{0}]", domainId);
			return domain.getInternetName();
		}
	}
	
	private OGroup doGroupInsert(Connection con, String domainId, String groupId, String displayName) throws DAOException, WTException {
		GroupEntity ge = new GroupEntity();
		ge.setGroupId(groupId);
		ge.setDisplayName(displayName);
		return doGroupInsert(con, domainId, ge);
	}
	
	private OGroup doGroupInsert(Connection con, String domainId, GroupEntity group) throws DAOException, WTException {
		GroupDAO dao = GroupDAO.getInstance();
		
		// Insert Group record
		logger.debug("Inserting group... [{}]", group.getGroupId());
		OGroup ogroup = new OGroup();
		ogroup.setDomainId(domainId);
		ogroup.setGroupId(group.getGroupId());
		ogroup.setEnabled(true);
		ogroup.setGroupUid(IdentifierUtils.getUUID());
		ogroup.setDisplayName(group.getDisplayName());
		ogroup.setSecret(null);
		dao.insert(con, ogroup);
		
		logger.debug("Inserting users associations...");
		HashSet<String> usedUserUids = new HashSet<>();
		for (AssignedUser assiUser : group.getAssignedUsers()) {
			final String userUid = userToUid(new UserProfileId(group.getDomainId(), assiUser.getUserId()));
			if (!usedUserUids.contains(userUid)) { // Avoid userUid duplicates
				doInsertUserAssociation(con, userUid, ogroup.getGroupUid());
				usedUserUids.add(userUid);
			}
		}
		
		logger.debug("Inserting roles associations");
		HashSet<String> usedRoleUids = new HashSet<>();
		for(AssignedRole assiRole : group.getAssignedRoles()) {
			final String roleUid = assiRole.getRoleUid();
			if (!usedRoleUids.contains(roleUid)) { // Avoid roles duplicates
				doInsertRoleAssociation(con, ogroup.getGroupUid(), roleUid);
				usedRoleUids.add(roleUid);
			}
		}
		
		// Insert permissions
		logger.debug("Inserting permissions");
		for(ORolePermission perm : group.getPermissions()) {
			doInsertPermission(con, ogroup.getGroupUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
		}
		for(ORolePermission perm : group.getServicesPermissions()) {
			doInsertPermission(con, ogroup.getGroupUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
		}
		
		return ogroup;
	}
	
	private void doGroupUpdate(Connection con, GroupEntity group) throws DAOException, WTException {
		GroupDAO dao = GroupDAO.getInstance();
		UserAssociationDAO uadao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		RolePermissionDAO rpdao = RolePermissionDAO.getInstance();
		
		GroupEntity oldGroup = getGroupEntity(con, group.getProfileId());
		if(oldGroup == null) throw new WTException("Group not found [{0}]", group.getProfileId().toString());
		
		logger.debug("Updating group");
		OGroup ogroup = new OGroup();
		ogroup.setDomainId(group.getDomainId());
		ogroup.setGroupId(group.getGroupId());
		ogroup.setDisplayName(group.getDisplayName());
		dao.update(con, ogroup);
		
		logger.debug("Updating users associations");
		LangUtils.CollectionChangeSet<AssignedUser> changeSet1 = LangUtils.getCollectionChanges(oldGroup.getAssignedUsers(), group.getAssignedUsers());
		for(AssignedUser assiUser : changeSet1.deleted) {
			uadao.deleteById(con, assiUser.getUserAssociationId());
		}
		for(AssignedUser assiUser : changeSet1.inserted) {
			final String userUid = userToUid(new UserProfileId(group.getDomainId(), assiUser.getUserId()));
			doInsertUserAssociation(con, userUid, oldGroup.getGroupUid());
		}
		
		logger.debug("Updating roles associations");
		LangUtils.CollectionChangeSet<AssignedRole> changeSet2 = LangUtils.getCollectionChanges(oldGroup.getAssignedRoles(), group.getAssignedRoles());
		for(AssignedRole assiRole : changeSet2.deleted) {
			rolassdao.deleteById(con, assiRole.getRoleAssociationId());
		}
		for(AssignedRole assiRole : changeSet2.inserted) {
			doInsertRoleAssociation(con, oldGroup.getGroupUid(), assiRole.getRoleUid());
		}

		logger.debug("Updating permissions");
		LangUtils.CollectionChangeSet<ORolePermission> changeSet3 = LangUtils.getCollectionChanges(oldGroup.getPermissions(), group.getPermissions());
		for(ORolePermission perm : changeSet3.deleted) {
			rpdao.deleteById(con, perm.getRolePermissionId());
		}
		for(ORolePermission perm : changeSet3.inserted) {
			doInsertPermission(con, oldGroup.getGroupUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
		}

		LangUtils.CollectionChangeSet<ORolePermission> changeSet4 = LangUtils.getCollectionChanges(oldGroup.getServicesPermissions(), group.getServicesPermissions());
		for(ORolePermission perm : changeSet4.deleted) {
			rpdao.deleteById(con, perm.getRolePermissionId());
		}
		for(ORolePermission perm : changeSet4.inserted) {
			doInsertPermission(con, oldGroup.getGroupUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
		}
	}
	
	private AuthUser createAuthUser(UserEntity user) {
		return new AuthUser(user.getUserId(), user.getDisplayName(), user.getFirstName(), user.getLastName(), null);
	}
	
	private OUser doUserInsert(Connection con, ODomain domain, UserEntity user) throws DAOException, WTException {
		UserDAO udao = UserDAO.getInstance();
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		
		InternetAddress email = InternetAddressUtils.toInternetAddress(user.getUserId(), domain.getInternetName(), null);
		if(email == null) throw new WTException("Cannot create a valid email address [{0}, {1}]", user.getUserId(), domain.getInternetName());
		
		// Insert User record
		logger.debug("Inserting user");
		OUser ouser = new OUser();
		ouser.setDomainId(domain.getDomainId());
		ouser.setUserId(user.getUserId());
		ouser.setEnabled(user.getEnabled());
		ouser.setUserUid(IdentifierUtils.getUUID());
		ouser.setDisplayName(user.getDisplayName());
		ouser.setSecret(generateSecretKey());
		udao.insert(con, ouser);
		
		// Insert UserInfo record
		logger.debug("Inserting userInfo");
		OUserInfo oui = new OUserInfo();
		oui.setDomainId(domain.getDomainId());
		oui.setUserId(user.getUserId());
		oui.setFirstName(user.getFirstName());
		oui.setLastName(user.getLastName());
		oui.setEmail(email.getAddress());
		uidao.insert(con, oui);
		
		logger.debug("Inserting groups associations");
		HashSet<String> usedGroupUids = new HashSet<>();
		for(AssignedGroup assiGroup : user.getAssignedGroups()) {
			final String groupUid = groupToUid(new UserProfileId(user.getDomainId(), assiGroup.getGroupId()));
			if (!usedGroupUids.contains(groupUid)) {
				// Due to built-in assigned groups, collection of assigned
				// groups can contain duplicates; so skip them.
				doInsertUserAssociation(con, ouser.getUserUid(), groupUid);
				usedGroupUids.add(groupUid);
			}
		}
		
		logger.debug("Inserting roles associations");
		HashSet<String> usedRoleUids = new HashSet<>();
		for(AssignedRole assiRole : user.getAssignedRoles()) {
			final String roleUid = assiRole.getRoleUid();
			if (!usedRoleUids.contains(roleUid)) {
				// Due to built-in assigned roles, collection of assigned
				// roles can contain duplicates; so skip them.
				doInsertRoleAssociation(con, ouser.getUserUid(), roleUid);
				usedRoleUids.add(roleUid);
			}
		}
		
		// Insert permissions
		logger.debug("Inserting permissions");
		for(ORolePermission perm : user.getPermissions()) {
			doInsertPermission(con, ouser.getUserUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
		}
		for(ORolePermission perm : user.getServicesPermissions()) {
			doInsertPermission(con, ouser.getUserUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
		}
		
		return ouser;
	}
	
	private void doUserUpdate(Connection con, UserEntity user) throws DAOException, WTException {
		UserDAO udao = UserDAO.getInstance();
		UserInfoDAO uidao = UserInfoDAO.getInstance();
		UserAssociationDAO uadao = UserAssociationDAO.getInstance();
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		RolePermissionDAO rpdao = RolePermissionDAO.getInstance();
		
		UserEntity oldUser = getUserEntity(con, user.getProfileId());
		if(oldUser == null) throw new WTException("User not found [{0}]", user.getProfileId().toString());
		
		logger.debug("Updating user");
		OUser ouser = new OUser();
		ouser.setDomainId(user.getDomainId());
		ouser.setUserId(user.getUserId());
		ouser.setEnabled(user.getEnabled());
		ouser.setDisplayName(user.getDisplayName());
		udao.updateEnabledDisplayName(con, ouser);

		logger.debug("Updating userInfo");
		OUserInfo ouseri = new OUserInfo();
		ouseri.setDomainId(user.getDomainId());
		ouseri.setUserId(user.getUserId());
		ouseri.setFirstName(user.getFirstName());
		ouseri.setLastName(user.getLastName());
		uidao.updateFirstLastName(con, ouseri);
		
		logger.debug("Updating groups associations");
		LangUtils.CollectionChangeSet<AssignedGroup> changeSet1 = LangUtils.getCollectionChanges(oldUser.getAssignedGroups(), user.getAssignedGroups());
		for(AssignedGroup assiGroup : changeSet1.deleted) {
			uadao.deleteById(con, assiGroup.getUserAssociationId());
		}
		for(AssignedGroup assiGroup : changeSet1.inserted) {
			final String groupUid = groupToUid(new UserProfileId(user.getDomainId(), assiGroup.getGroupId()));
			doInsertUserAssociation(con, oldUser.getUserUid(), groupUid);
		}
		
		logger.debug("Updating roles associations");
		LangUtils.CollectionChangeSet<AssignedRole> changeSet2 = LangUtils.getCollectionChanges(oldUser.getAssignedRoles(), user.getAssignedRoles());
		for(AssignedRole assiRole : changeSet2.deleted) {
			rolassdao.deleteById(con, assiRole.getRoleAssociationId());
		}
		for(AssignedRole assiRole : changeSet2.inserted) {
			doInsertRoleAssociation(con, oldUser.getUserUid(), assiRole.getRoleUid());
		}

		logger.debug("Updating permissions");
		LangUtils.CollectionChangeSet<ORolePermission> changeSet3 = LangUtils.getCollectionChanges(oldUser.getPermissions(), user.getPermissions());
		for(ORolePermission perm : changeSet3.deleted) {
			rpdao.deleteById(con, perm.getRolePermissionId());
		}
		for(ORolePermission perm : changeSet3.inserted) {
			doInsertPermission(con, oldUser.getUserUid(), perm.getServiceId(), perm.getKey(), perm.getAction(), "*");
		}

		LangUtils.CollectionChangeSet<ORolePermission> changeSet4 = LangUtils.getCollectionChanges(oldUser.getServicesPermissions(), user.getServicesPermissions());
		for(ORolePermission perm : changeSet4.deleted) {
			rpdao.deleteById(con, perm.getRolePermissionId());
		}
		for(ORolePermission perm : changeSet4.inserted) {
			doInsertPermission(con, oldUser.getUserUid(), CoreManifest.ID, "SERVICE", ServicePermission.ACTION_ACCESS, perm.getInstance());
		}
	}
	
	private void fillDomain(ODomain o, DomainEntity domain) throws WTException {
		o.setDomainId(domain.getDomainId());
		o.setInternetName(domain.getInternetName());
		o.setEnabled(domain.getEnabled());
		o.setDescription(domain.getDescription());
		o.setUserAutoCreation(domain.getUserAutoCreation());
		o.setDirUri(domain.getDirUri().toString());
		
		String scheme = domain.getDirUri().getScheme();
		if (scheme.equals(WebTopDirectory.SCHEME)) {
			o.setDirConnectionSecurity(null);
			o.setDirAdmin(null);
			o.setDirPassword(null);
			o.setDirPasswordPolicy(domain.getDirPasswordPolicy());
			
		} else if (scheme.equals(LdapWebTopDirectory.SCHEME)) {
			o.setDirConnectionSecurity(EnumUtils.getName(domain.getDirConnSecurity()));
			o.setDirAdmin(domain.getDirAdmin());
			setDirPassword(o, domain.getDirPassword());
			o.setDirPasswordPolicy(domain.getDirPasswordPolicy());
			
		} else if (scheme.equals(LdapDirectory.SCHEME)) {
			o.setDirConnectionSecurity(EnumUtils.getName(domain.getDirConnSecurity()));
			o.setDirAdmin(domain.getDirAdmin());
			setDirPassword(o, domain.getDirPassword());
			
			o.setDirPasswordPolicy(false);
		} else if (scheme.equals(ImapDirectory.SCHEME)) {
			o.setDirConnectionSecurity(EnumUtils.getName(domain.getDirConnSecurity()));
			o.setDirAdmin(null);
			o.setDirPassword(null);
			o.setDirPasswordPolicy(false);
			
		} else if (scheme.equals(SmbDirectory.SCHEME) || scheme.equals(SftpDirectory.SCHEME)) {
			o.setDirConnectionSecurity(null);
			o.setDirAdmin(null);
			o.setDirPassword(null);
			o.setDirPasswordPolicy(false);
			
		} else if (scheme.equals(ADDirectory.SCHEME)) {
			o.setDirConnectionSecurity(EnumUtils.getName(domain.getDirConnSecurity()));
			o.setDirAdmin(domain.getDirAdmin());
			setDirPassword(o, domain.getDirPassword());
			o.setDirPasswordPolicy(domain.getDirPasswordPolicy());
			
		} else if (scheme.equals(LdapNethDirectory.SCHEME)) {
			o.setDirConnectionSecurity(EnumUtils.getName(domain.getDirConnSecurity()));
			o.setDirAdmin(domain.getDirAdmin());
			setDirPassword(o, domain.getDirPassword());
			o.setDirPasswordPolicy(false);
		}
		o.setDirCaseSensitive(domain.getDirCaseSensitive());
		if (domain.getDirParameters() instanceof ParamsLdapDirectory) {
			o.setDirParameters(LangUtils.serialize(domain.getDirParameters(), ParamsLdapDirectory.class));
		} else {
			o.setDirParameters(null);
		}
	}
	
	private OUserAssociation doInsertUserAssociation(Connection con, String userUid, String groupUid) throws WTException {
		UserAssociationDAO uadao = UserAssociationDAO.getInstance();
		
		OUserAssociation oua = new OUserAssociation();
		oua.setUserAssociationId(uadao.getSequence(con).intValue());
		oua.setUserUid(userUid);
		oua.setGroupUid(groupUid);
		uadao.insert(con, oua);
		return oua;
	}
	
	private ORoleAssociation doInsertRoleAssociation(Connection con, String userUid, String roleUid) throws WTException {
		RoleAssociationDAO rolassdao = RoleAssociationDAO.getInstance();
		
		ORoleAssociation ora = new ORoleAssociation();
		ora.setRoleAssociationId(rolassdao.getSequence(con).intValue());
		ora.setUserUid(userUid);
		ora.setRoleUid(roleUid);
		rolassdao.insert(con, ora);
		return ora;
	}
	
	private ORolePermission doInsertPermission(Connection con, String roleUid, String serviceId, String key, String action, String instance) throws WTException {
		RolePermissionDAO rpdao = RolePermissionDAO.getInstance();
		
		ORolePermission perm = new ORolePermission();
		perm.setRolePermissionId(rpdao.getSequence(con).intValue());
		perm.setRoleUid(roleUid);
		perm.setServiceId(serviceId);
		perm.setKey(key);
		perm.setAction(action);
		perm.setInstance(instance);
		
		rpdao.insert(con, perm);
		return perm;
	}
	
	private UserProfile.Data getUserData(UserProfileId pid) throws WTException {
		OUser ouser = getUser(pid);
		if (ouser == null) return null;
		
		String internetName = WT.getDomainInternetName(pid.getDomainId());
		CoreUserSettings cus = new CoreUserSettings(pid);
		UserProfile.PersonalInfo upi = userPersonalInfo(pid);
		
		DomainAccount internetAccount = new DomainAccount(internetName, pid.getUserId());
		InternetAddress profileIa = InternetAddressUtils.toInternetAddress(pid.getUserId(), internetName, ouser.getDisplayName());
		InternetAddress personalIa = InternetAddressUtils.toInternetAddress(upi.getEmail(), ouser.getDisplayName());
		if (personalIa == null) {
			personalIa = profileIa;
			logger.warn("User does not have a valid email in personal info. Check it! [{}]", pid.toString());
		}
		return new UserProfile.Data(internetAccount, ouser.getDisplayName(), cus.getLanguageTag(), cus.getTimezone(), 
				cus.getStartDay(), cus.getShortDateFormat(), cus.getLongDateFormat(), cus.getShortTimeFormat(), cus.getLongTimeFormat(), 
				profileIa, personalIa);
	}
	
	private OUserInfo createUserInfo(UserProfile.PersonalInfo upi) {
		OUserInfo oui = new OUserInfo();
		oui.setTitle(upi.getTitle());
		oui.setFirstName(upi.getFirstName());
		oui.setLastName(upi.getLastName());
		oui.setNickname(upi.getNickname());
		oui.setGender(upi.getGender());
		oui.setEmail(upi.getEmail());
		oui.setTelephone(upi.getTelephone());
		oui.setFax(upi.getFax());
		oui.setPager(upi.getPager());
		oui.setMobile(upi.getMobile());
		oui.setAddress(upi.getAddress());
		oui.setCity(upi.getCity());
		oui.setPostalCode(upi.getPostalCode());
		oui.setState(upi.getState());
		oui.setCountry(upi.getCountry());
		oui.setCompany(upi.getCompany());
		oui.setFunction(upi.getFunction());
		oui.setCustom1(upi.getCustom01());
		oui.setCustom2(upi.getCustom02());
		oui.setCustom3(upi.getCustom03());
		return oui;
	}
	
	private String getDirPassword(ODomain o) {
		return PasswordUtils.decryptDES(o.getDirPassword(), new String(new char[]{'p','a','s','s','w','o','r','d'}));
	}
	
	private void setDirPassword(ODomain o, String password) {
		o.setDirPassword(PasswordUtils.encryptDES(password, new String(new char[]{'p','a','s','s','w','o','r','d'})));
	}
	
	private void initDomainCache() {
		Connection con = null;
		
		try {
			synchronized(lock0) {
				DomainDAO dao = DomainDAO.getInstance();
				
				con = wta.getConnectionManager().getConnection();
				cleanupDomainCache();
				for(ODomain odomain : dao.selectEnabled(con)) {
					cachePublicNameToDomain.put(domainIdToPublicName(odomain.getDomainId()), odomain.getDomainId());
					cacheInternetNameToDomain.put(odomain.getInternetName(), odomain.getDomainId());
				}
			}
		} catch(SQLException ex) {
			throw new WTRuntimeException(ex, "Unable to init domain name cache");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private void cleanupDomainCache() {
		synchronized(lock0) {
			cachePublicNameToDomain.clear();
			cacheInternetNameToDomain.clear();
		}
	}
	
	private void cleanupUserCache() {
		synchronized(cacheUserToData) {
			cacheUserToData.clear();
		}
		synchronized(cacheUserToPersonalInfo) {
			cacheUserToPersonalInfo.clear();
		}
	}
	
	private void addToUserCache(UserProfileId pid, UserProfile.Data userData) {
		synchronized(cacheUserToData) {
			cacheUserToData.put(pid, userData);
		}
	}
	
	private void addToUserCache(UserProfileId pid, UserProfile.PersonalInfo userPersonalInfo) {
		synchronized(cacheUserToPersonalInfo) {
			cacheUserToPersonalInfo.put(pid, userPersonalInfo);
		}
	}
	
	private void removeFromUserCache(UserProfileId pid) {
		synchronized(cacheUserToData) {
			cacheUserToData.remove(pid);
		}
		synchronized(cacheUserToPersonalInfo) {
			cacheUserToPersonalInfo.remove(pid);
		}
	}
	
	private void initUserUidCache() {
		Connection con = null;
		
		try {
			synchronized(lock1) {
				UserDAO dao = UserDAO.getInstance();
				
				con = wta.getConnectionManager().getConnection();
				List<UserUid> uids = dao.viewAllUids(con);
				cleanupUserUidCache();
				for(UserUid uid : uids) {
					addToUserUidCache(uid);
				}
			}
		} catch(SQLException ex) {
			throw new WTRuntimeException(ex, "Unable to init user UID cache");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private void cleanupUserUidCache() {
		synchronized(lock1) {
			cacheUserToUserUid.clear();
			cacheUserUidToUser.clear();
		}
	}
	
	private void addToUserUidCache(UserUid uid) {
		synchronized(lock1) {
			UserProfileId pid = new UserProfileId(uid.getDomainId(), uid.getUserId());
			cacheUserToUserUid.put(pid, uid.getUserUid());
			cacheUserUidToUser.put(uid.getUserUid(), pid);
		}
	}
	
	private void removeFromUserUidCache(UserProfileId pid) {
		synchronized(lock1) {
			if(cacheUserToUserUid.containsKey(pid)) {
				String uid = cacheUserToUserUid.remove(pid);
				cacheUserUidToUser.remove(uid);
			}
		}
	}
	
	private void initGroupUidCache() {
		Connection con = null;
		
		try {
			synchronized(lock2) {
				GroupDAO dao = GroupDAO.getInstance();
				
				con = wta.getConnectionManager().getConnection();
				List<GroupUid> uids = dao.viewAllUids(con);
				cleanupGroupUidCache();
				for(GroupUid uid : uids) {
					addToGroupUidCache(uid);
				}
			}
		} catch(SQLException ex) {
			throw new WTRuntimeException(ex, "Unable to init group UID cache");
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	private void cleanupGroupUidCache() {
		synchronized(lock2) {
			cacheGroupToGroupUid.clear();
			cacheGroupUidToGroup.clear();
		}
	}
	
	private void addToGroupUidCache(GroupUid uid) {
		synchronized(lock2) {
			UserProfileId pid = new UserProfileId(uid.getDomainId(), uid.getUserId());
			cacheGroupToGroupUid.put(pid, uid.getUserUid());
			cacheGroupUidToGroup.put(uid.getUserUid(), pid);
		}
	}
	
	private void removeFromGroupUidCache(UserProfileId pid) {
		synchronized(lock2) {
			if(cacheGroupToGroupUid.containsKey(pid)) {
				String uid = cacheGroupToGroupUid.remove(pid);
				cacheGroupUidToGroup.remove(uid);
			}
		}
	}
	
    private void createCyrusUser(String login,String domainId) throws WTCyrusException {
		String host=wta.getSettingsManager().getServiceSetting(domainId, "com.sonicle.webtop.mail", BaseServiceSettings.DEFAULT_PREFIX+"host");
		int port=Integer.parseInt(wta.getSettingsManager().getServiceSetting(domainId, "com.sonicle.webtop.mail", BaseServiceSettings.DEFAULT_PREFIX+"port"));
		String protocol=wta.getSettingsManager().getServiceSetting(domainId, "com.sonicle.webtop.mail", BaseServiceSettings.DEFAULT_PREFIX+"protocol");
		String adminuser=wta.getSettingsManager().getServiceSetting(domainId, "com.sonicle.webtop.mail", "admin.user");
		String adminpass=wta.getSettingsManager().getServiceSetting(domainId, "com.sonicle.webtop.mail", "admin.password");
		Store s=getCyrusStore(host,port,protocol,adminuser,adminpass);
		createCyrusMailbox(login, s);
		setCyrusAcl(login, login, s);
		setCyrusAcl(login, adminuser, s);
		try { s.close(); } catch(Exception exc) { }
    }
    
    private Store getCyrusStore(String host,int port,String protocol,String user,String psw) throws WTCyrusException {
        Properties props = new Properties(wta.getProperties());
		props.setProperty("mail.store.protocol", protocol);
		props.setProperty("mail.store.port", ""+port);
        Session session = Session.getInstance(props, null);
		try {
			Store store = session.getStore(protocol);
			store.connect(host,user,psw);  
			return store;
		} catch(Exception exc) {
			throw new WTCyrusException(exc);
		}
	}
	
    private void createCyrusMailbox(String login, Store store) throws WTCyrusException {
		try {
			char sep=store.getDefaultFolder().getSeparator();
            Folder c=store.getFolder("user"+sep+login);
            if (!c.exists())
                c.create(Folder.HOLDS_FOLDERS);
		} catch(Exception exc) {
			throw new WTCyrusException(exc);
		}
    }
	
    public void setCyrusAcl(String login, String acllogin, Store store) throws WTCyrusException {
        try{
			char sep=store.getDefaultFolder().getSeparator();
            Folder f= store.getFolder("user"+sep+login);
            IMAPFolder folder=(IMAPFolder)f;
            Rights r=new Rights("lrswipcda");
            ACL a=new ACL(acllogin,r);
            folder.addACL(a); 
        } catch(Exception exc){
			throw new WTCyrusException(exc);
        }
    }
	
	public static class CheckUserResult {
		public boolean exist;
		public boolean enabled;
		
		public CheckUserResult(boolean exist, boolean enabled) {
			this.exist = exist;
			this.enabled = enabled;
		}
	}
	
	public static class UserUidBag {
		public String userUid;
		public String roleUid;
		
		public UserUidBag() {}
		
		public UserUidBag(String uid, String roleUid) {
			this.userUid = uid;
			this.roleUid = roleUid;
		}
	}
	
	public static class EntityPermissions {
		public ArrayList<ORolePermission> others;
		public ArrayList<ORolePermission> services;
		
		public EntityPermissions(ArrayList<ORolePermission> others, ArrayList<ORolePermission> services) {
			this.others = others;
			this.services = services;
		}
	}
	
	protected WTException wrapThrowable(Throwable t) {
		if (t instanceof WTException) {
			return (WTException)t;
		} else if ((t instanceof SQLException) || (t instanceof DAOException)) {
			return new WTException(t, "DB error");
		} else {
			return new WTException(t);
		}
	}
}
