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

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.commons.web.Crud;
import com.sonicle.commons.web.ParameterException;
import com.sonicle.commons.web.json.JsonResult;
import com.sonicle.commons.web.ServletUtils;
import com.sonicle.commons.web.json.PayloadAsList;
import com.sonicle.commons.web.json.MapItem;
import com.sonicle.commons.web.json.Payload;
import com.sonicle.security.Principal;
import com.sonicle.security.auth.directory.AbstractDirectory;
import com.sonicle.security.auth.directory.DirectoryCapability;
import com.sonicle.webtop.core.CoreSettings.OtpDeliveryMode;
import com.sonicle.webtop.core.admin.CoreAdminManager;
import com.sonicle.webtop.core.app.CoreAdminManifest;
import com.sonicle.webtop.core.app.CoreManifest;
import com.sonicle.webtop.core.app.RunContext;
import com.sonicle.webtop.core.app.CorePrivateEnvironment;
import com.sonicle.webtop.core.app.OTPManager;
import com.sonicle.webtop.core.app.WT;
import com.sonicle.webtop.core.app.WebTopManager;
import com.sonicle.webtop.core.app.WebTopSession;
import com.sonicle.webtop.core.app.provider.RecipientsProviderBase;
import com.sonicle.webtop.core.msg.IMChatRoomAdded;
import com.sonicle.webtop.core.msg.IMChatRoomMessageReceived;
import com.sonicle.webtop.core.msg.IMChatRoomRemoved;
import com.sonicle.webtop.core.msg.IMChatRoomClosed;
import com.sonicle.webtop.core.msg.IMChatRoomUpdated;
import com.sonicle.webtop.core.msg.IMFriendPresenceUpdated;
import com.sonicle.webtop.core.msg.IMFriendsUpdated;
import com.sonicle.webtop.core.bol.OAutosave;
import com.sonicle.webtop.core.bol.ODomain;
import com.sonicle.webtop.core.bol.OUser;
import com.sonicle.webtop.core.bol.js.JsActivityLkp;
import com.sonicle.webtop.core.bol.js.JsAutosave;
import com.sonicle.webtop.core.bol.js.JsCausalLkp;
import com.sonicle.webtop.core.bol.js.JsCustomerSupplierLkp;
import com.sonicle.webtop.core.bol.js.JsSimple;
import com.sonicle.webtop.core.bol.js.JsFeedback;
import com.sonicle.webtop.core.bol.js.JsGridIMChat;
import com.sonicle.webtop.core.bol.js.JsGridIMFriend;
import com.sonicle.webtop.core.bol.js.JsGridIMMessage;
import com.sonicle.webtop.core.bol.js.JsGridIMChatSearch;
import com.sonicle.webtop.core.bol.js.JsGroupChat;
import com.sonicle.webtop.core.bol.js.JsIMInit;
import com.sonicle.webtop.core.bol.js.JsIMPresenceStatus;
import com.sonicle.webtop.core.bol.js.JsInternetAddress;
import com.sonicle.webtop.core.bol.js.JsPublicImage;
import com.sonicle.webtop.core.bol.js.JsReminderInApp;
import com.sonicle.webtop.core.bol.js.JsRoleLkp;
import com.sonicle.webtop.core.bol.js.JsServicePermissionLkp;
import com.sonicle.webtop.core.bol.model.UserOptionsServiceData;
import com.sonicle.webtop.core.bol.js.JsTrustedDevice;
import com.sonicle.webtop.core.bol.js.JsWhatsnewTab;
import com.sonicle.webtop.core.bol.js.TrustedDeviceCookie;
import com.sonicle.webtop.core.model.Recipient;
import com.sonicle.webtop.core.bol.model.Role;
import com.sonicle.webtop.core.bol.model.RoleWithSource;
import com.sonicle.webtop.core.model.ServicePermission;
import com.sonicle.webtop.core.model.Activity;
import com.sonicle.webtop.core.model.Causal;
import com.sonicle.webtop.core.model.CausalExt;
import com.sonicle.webtop.core.model.IMChat;
import com.sonicle.webtop.core.model.IMMessage;
import com.sonicle.webtop.core.model.MasterData;
import com.sonicle.webtop.core.model.PublicImage;
import com.sonicle.webtop.core.model.RecipientFieldType;
import com.sonicle.webtop.core.util.AppLocale;
import com.sonicle.webtop.core.sdk.BaseService;
import com.sonicle.webtop.core.sdk.UserProfile;
import com.sonicle.webtop.core.sdk.ServiceMessage;
import com.sonicle.webtop.core.sdk.UploadException;
import com.sonicle.webtop.core.sdk.UserProfileId;
import com.sonicle.webtop.core.sdk.WTException;
import com.sonicle.webtop.core.sdk.interfaces.IServiceUploadStreamListener;
import com.sonicle.webtop.core.xmpp.ChatMember;
import com.sonicle.webtop.core.xmpp.Friend;
import com.sonicle.webtop.core.xmpp.FriendPresence;
import com.sonicle.webtop.core.xmpp.ChatMessage;
import com.sonicle.webtop.core.xmpp.ChatRoom;
import com.sonicle.webtop.core.xmpp.ConversationHistory;
import com.sonicle.webtop.core.xmpp.InstantChatRoom;
import com.sonicle.webtop.core.xmpp.GroupChatRoom;
import com.sonicle.webtop.core.xmpp.PresenceStatus;
import com.sonicle.webtop.core.xmpp.XMPPClient;
import com.sonicle.webtop.core.xmpp.XMPPClientException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.SortedMap;
import java.util.TimeZone;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.slf4j.Logger;
import com.sonicle.webtop.core.xmpp.XMPPClientListener;
import com.sonicle.webtop.core.xmpp.XMPPHelper;
import com.sonicle.webtop.core.xmpp.packet.OutOfBandData;
import com.sonicle.webtop.vfs.IVfsManager;
import com.sonicle.webtop.vfs.model.SharingLink;
import java.io.StringReader;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import org.apache.commons.vfs2.FileObject;
import org.apache.shiro.io.ResourceUtils;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.jxmpp.jid.EntityFullJid;

/**
 *
 * @author malbinola
 */
public class Service extends BaseService {
	private static final Logger logger = WT.getLogger(Service.class);
	public static final String WTSPROP_OTP_SETUP = "OTPSETUP";
	
	private CoreManager coreMgr;
	private CoreServiceSettings ss;
	private CoreUserSettings us;
	private XMPPClient xmppCli;
	
	/*
	private WebTopApp getApp() {
		return ((CorePrivateEnvironment)getEnv()).getApp();
	}
	*/
	
	@Override
	public void initialize() throws Exception {
		final UserProfile profile = getEnv().getProfile();
		UserProfileId pid = profile.getId();
		coreMgr = getCoreManager();
		ss = new CoreServiceSettings(SERVICE_ID, pid.getDomainId());
		us = new CoreUserSettings(pid);
		
		registerUploadListener(UPLOAD_CONTEXT_WEBCHAT, new OnUploadCloudFile());
		
		Principal principal = profile.getPrincipal();
		if (!principal.isImpersonated()) {
			ConversationHistory history = new ConversationHistory();
			for(IMChat chat : coreMgr.listIMChats(true)) {
				if (!chat.getIsGroupChat()) {
					history.addChat(createDirectChatRoom(chat));
				} else {
					final List<String> stanzaIDs = coreMgr.listIMMessageStanzaIDs(chat.getChatJid());
					history.addChat(createGroupChatRoom(chat), stanzaIDs);
				}
			}
			
			final String xmppResource = getWts().getId() + "@" + WT.getPlatformName();
			final String internetName = WT.getDomainInternetName(pid.getDomainId());
			XMPPTCPConnectionConfiguration.Builder builder = XMPPHelper.setupConfigBuilder(ss.getXMPPHost(), ss.getXMPPPort(), internetName, principal.getUserId(), new String(principal.getPassword()), xmppResource);
			final String nickname = profile.getDisplayName();
			xmppCli = new XMPPClient(builder, ss.getXMPPMucSubdomain(), nickname, new XMPPServiceListenerImpl(), history);
		}
		
		//sendAuthMessage(principal.getUserId(),principal.getPassword());
	}
	
	//private void sendAuthMessage(String userId, char password[]) {
	//	getEnv().notify(new AuthMessage(SERVICE_ID,userId,password));		
	//}
	
	private CoreManager getCoreManager() {
		return (CoreManager)WT.getServiceManager(SERVICE_ID);
	}
	
	private CoreAdminManager getCoreAdminManager() {
		return (CoreAdminManager)WT.getServiceManager(CoreAdminManifest.ID);
	}
	
	private IVfsManager getVfsManager() {
		return (IVfsManager)WT.getServiceManager("com.sonicle.webtop.vfs");
	}

	@Override
	public void cleanup() throws Exception {
		if (xmppCli != null) {
			xmppCli.disconnect();
		}
	}

	@Override
	public ServiceVars returnServiceVars() {
		UserProfile profile = getEnv().getProfile();
		ServiceVars co = new ServiceVars();
		
		boolean domainPasswordPolicy = false;
		boolean dirCapPasswordWrite = false;
		try {
			if (profile.getDomainId().equals(WebTopManager.SYSADMIN_DOMAINID)) {
				dirCapPasswordWrite = true;
			} else {
				ODomain domain = coreMgr.getDomain();
				if (domain != null) {
					domainPasswordPolicy = domain.getDirPasswordPolicy();
					AbstractDirectory dir = coreMgr.getAuthDirectory(domain);
					dirCapPasswordWrite = dir.hasCapability(DirectoryCapability.PASSWORD_WRITE);
				}
			}
		} catch(WTException ex) {}
		
		boolean docServerEnabled = getDocumentServerEnabled();
		co.put("docServerEnabled", docServerEnabled);
		if (docServerEnabled) {
			co.put("docServerPublicUrl", ss.getDocumentServerPublicUrl());
		}
		String boshUrl = ss.getXMPPBoshUrl();
		if (!StringUtils.isBlank(boshUrl)) {
			co.put("boshUrl", boshUrl);
		}
		String iceServers = ss.getWebRTC_ICEServers();
		if (iceServers != null) {
			co.put("iceServers", iceServers);
		}
		
		co.put("wtAddonNotifier", addonNotifier());
		co.put("wtWhatsnewEnabled", ss.getWhatsnewEnabled());
		//co.put("wtForcePasswordChange", ss.getOTPEnabled());
		co.put("wtOtpEnabled", ss.getOTPEnabled());
		co.put("wtLauncherLinks", ss.getLauncherLinksAsString());
		
		
		for(AppLocale apploc : WT.getInstalledLocales()) {
			String patterns=lookupResource(apploc.getLocale(),CoreLocaleKey.DETECT_ATTACH_PATTERNS);
			if (patterns!=null && !patterns.equals(CoreLocaleKey.DETECT_ATTACH_PATTERNS))
				co.put("editorAttachPatterns-"+apploc.getId().substring(0,2), patterns);
		}
		
		co.put("domainPasswordPolicy", domainPasswordPolicy);
		co.put("domainDirCapPasswordWrite", dirCapPasswordWrite);
		co.put("domainInternetName", WT.getDomainInternetName(profile.getDomainId()));
		co.put("profileId", profile.getStringId());
		co.put("domainId", profile.getDomainId());
		co.put("userId", profile.getUserId());
		co.put("userDisplayName", profile.getDisplayName());
		co.put("editorFonts", ss.getEditorFonts());
		co.put("theme", us.getTheme());
		co.put("laf", us.getLookAndFeel());
		co.put("layout", us.getLayout());
		co.put("viewportHeaderScale", EnumUtils.toSerializedName(us.getViewportHeaderScale()));
		co.put("startupService", us.getStartupService());
		co.put("desktopNotification", us.getDesktopNotification());
		
		co.put("ajaxSpecialTimeout", ss.getAjaxSpecialTimeout());
		co.put("language", us.getLanguageTag());
		co.put("timezone", us.getTimezone());
		co.put("startDay", us.getStartDay());
		co.put("shortDateFormat", us.getShortDateFormat());
		co.put("longDateFormat", us.getLongDateFormat());
		co.put("shortTimeFormat", us.getShortTimeFormat());
		co.put("longTimeFormat", us.getLongTimeFormat());
		co.put("imEnabled", !RunContext.isImpersonated() && RunContext.isPermitted(true, CoreManifest.ID, "WEBCHAT", "ACCESS"));
		co.put("imPresenceStatus", EnumUtils.toSerializedName(us.getIMPresenceStatus()));
		co.put("imStatusMessage", us.getIMStatusMessage());
		co.put("imUploadMaxFileSize", us.getIMUploadMaxFileSize(true));
		co.put("imSoundOnFriendConnect", us.getIMSoundOnFriendConnect());
		co.put("imSoundOnFriendDisconnect", us.getIMSoundOnFriendDisconnect());
		co.put("imSoundOnMessageReceived", us.getIMSoundOnMessageReceived());
		co.put("imSoundOnMessageSent", us.getIMSoundOnMessageSent());
		co.put("pbxConfigured",coreMgr.pbxConfigured());
		if (coreMgr.smsConfigured()) {
			co.put("smsConfigured",true);
			co.put("smsProvider",coreMgr.smsGetProvider().getName());
		}
		
		return co;
	}
	
	private String addonNotifier() {
		String url = ss.getAddonNotifierUrl();
		if (!StringUtils.isBlank(url)) return url;
		return LangUtils.classForNameQuietly("com.sonicle.webtop.addons.AddOns") ? "true" : "false";
	}
	
	private WebTopSession getWts() {
		return getEnv().getSession();
	}
	
	public void processActivateDebug(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		try {
			getWts().setJsDebugEnabled(true);
			new JsonResult().printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in ActivateDebug", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLogMessage(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String level = ServletUtils.getStringParameter(request, "level", "debug");
			String message = ServletUtils.getStringParameter(request, "message", true);
			
			if (level.equals("debug")) {
				logger.debug("{}", message);
			} else if (level.equals("info")) {
				logger.info("{}", message);
			} else if (level.equals("warn")) {
				logger.warn("{}", message);
			} else if (level.equals("error")) {
				logger.error("{}", message);
			} else {
				logger.trace("{}", message);
			}
			new JsonResult().printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LogMessage", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLookupLanguages(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		LinkedHashMap<String, JsSimple> items = new LinkedHashMap<>();
		Locale locale = getEnv().getSession().getLocale();
		
		try {
			for(AppLocale apploc : WT.getInstalledLocales()) {
				final Locale loc = apploc.getLocale();
				final String lang = loc.getLanguage();
				if(!items.containsKey(lang)) {
					//items.put(lang, new JsSimple(lang, loc.getDisplayLanguage(locale)));
					items.put(lang, new JsSimple(apploc.getId(), apploc.getLocale().getDisplayName(locale)));
				}
			}
			new JsonResult("languages", items.values(), items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupLanguages", ex);
			new JsonResult(false, "Unable to lookup languages").printTo(out);
		}
	}
	
	public void processLookupTimezones(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		ArrayList<JsSimple> items = new ArrayList<>();
		
		try {
			int off;
			for(TimeZone tz : WT.getTimezones()) {
				final String normId = StringUtils.replace(tz.getID(), "_", " ");
				off = tz.getRawOffset()/3600000;
				items.add(new JsSimple(tz.getID(), MessageFormat.format("{0} (GMT{1}{2})", normId, (off<0) ? "-" : "+", Math.abs(off))));
			}
			new JsonResult("timezones", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupTimezones", ex);
			new JsonResult(false, "Unable to lookup timezones").printTo(out);
		}
	}
	
	public void processLookupThemes(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = null;
		
		try {
			items = coreMgr.listThemes();
			new JsonResult("themes", items, items.size()).printTo(out);

		} catch (Exception ex) {
			logger.error("Error in LookupThemes", ex);
			new JsonResult(false, "Unable to lookup themes").printTo(out);
		}
	}
	
	public void processLookupLayouts(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = null;
		
		try {
			items = coreMgr.listLayouts();
			new JsonResult("layouts", items, items.size()).printTo(out);

		} catch (Exception ex) {
			logger.error("Error in LookupLayouts", ex);
			new JsonResult(false, "Unable to lookup layouts").printTo(out);
		}
	}
	
	public void processLookupLAFs(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = null;
		
		try {
			items = coreMgr.listLAFs();
			new JsonResult("lafs", items, items.size()).printTo(out);

		} catch (Exception ex) {
			logger.error("Error in LookupLAFs", ex);
			new JsonResult(false, "Unable to lookup look&feels").printTo(out);
		}
	}
	
	public void processLookupTextEncodings(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		ArrayList<JsSimple> items = new ArrayList<>();
		
		try {
			SortedMap<String, Charset> charsets = Charset.availableCharsets();
			for(Charset charset : charsets.values()) {
				if(!charset.canEncode()) continue;
				items.add(new JsSimple(charset.name(), charset.displayName(Locale.ENGLISH)));
			}
			new JsonResult("encodings", items, items.size()).printTo(out);

		} catch (Exception ex) {
			logger.error("Error in LookupTextEncodings", ex);
			new JsonResult(false, "Unable to lookup available text encodings").printTo(out);
		}
	}
	
	public void processLookupServices(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		Locale locale = getEnv().getSession().getLocale();
		ArrayList<JsSimple> items = new ArrayList<>();
		
		try {
			Boolean assignableOnly = ServletUtils.getBooleanParameter(request, "assignableOnly", false);
			
			for(String sid : coreMgr.listWTInstalledServices()) {
				if(assignableOnly && sid.equals(CoreManifest.ID)) continue;
				items.add(new JsSimple(sid, WT.lookupResource(sid, locale, BaseService.RESOURCE_SERVICE_NAME)));
			}
			new JsonResult(items).printTo(out);
			
		} catch(Exception ex) {
			logger.error("Error in LookupServices", ex);
			new JsonResult(false, "Error").printTo(out);
		}
	}
	
	public void processLookupServicesPermissions(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		ArrayList<JsServicePermissionLkp> items = new ArrayList<>();
		
		try {
			for(String sid : coreMgr.listWTInstalledServices()) {
				for(ServicePermission perm : coreMgr.listServicePermissions(sid)) {
					for(String action : perm.getActions()) {
						items.add(new JsServicePermissionLkp(sid, perm.getGroupName(), action));
					}
				}
			}
			new JsonResult(items).printTo(out);
			
		} catch(Exception ex) {
			logger.error("Error in LookupServicesPermissions", ex);
			new JsonResult(false, "Error").printTo(out);
		}
	}
	
	public void processLookupDomains(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = new ArrayList<>();
		UserProfile up = getEnv().getProfile();
		
		try {
			boolean wildcard = ServletUtils.getBooleanParameter(request, "wildcard", false);
			/*
			if(getRunContext().isSysAdmin()) {
				// WebTopAdmin can access to all domains
				if(wildcard) items.add(JsSimple.wildcard(lookupResource(up.getLocale(), CoreLocaleKey.WORD_ALL_MALE)));
				List<ODomain> domains = core.listDomains(true);
				for(ODomain domain : domains) {
					items.add(new JsSimple(domain.getDomainId(), JsSimple.description(domain.getDescription(), domain.getDomainId())));
				}
			} else {
				// Domain users can only access to their domain
				ODomain domain = core.getDomain(up.getDomainId());
				items.add(new JsSimple(domain.getDomainId(), JsSimple.description(domain.getDescription(), domain.getDomainId())));
			}
			*/
			
			if(wildcard && RunContext.isSysAdmin()) {
				items.add(JsSimple.wildcard(lookupResource(up.getLocale(), CoreLocaleKey.WORD_ALL_MALE)));
			}
			List<ODomain> domains = coreMgr.listDomains(true);
			for(ODomain domain : domains) {
				items.add(new JsSimple(domain.getDomainId(), JsSimple.description(domain.getDescription(), domain.getDomainId())));
			}
			new JsonResult("domains", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupDomains", ex);
			new JsonResult(false, "Unable to lookup domains").printTo(out);
		}
	}
	
	public void processLookupDomainRoles(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsRoleLkp> items = new ArrayList<>();
		UserProfile up = getEnv().getProfile();
		
		try {
			boolean wildcard = ServletUtils.getBooleanParameter(request, "wildcard", false);
			boolean users = ServletUtils.getBooleanParameter(request, "users", true);
			boolean groups = ServletUtils.getBooleanParameter(request, "groups", true);
			boolean roles = ServletUtils.getBooleanParameter(request, "roles", true);
			
			if(wildcard) items.add(JsRoleLkp.wildcard(lookupResource(up.getLocale(), CoreLocaleKey.WORD_ALL_MALE)));
			if(users) {
				for(Role role : coreMgr.listUsersRoles()) {
					items.add(new JsRoleLkp(role, RoleWithSource.SOURCE_USER));
				}
			}
			if(groups) {
				for(Role role : coreMgr.listGroupsRoles()) {
					items.add(new JsRoleLkp(role, RoleWithSource.SOURCE_GROUP));
				}
			}
			if (roles) {
				for(Role role : coreMgr.listRoles()) {
					items.add(new JsRoleLkp(role, RoleWithSource.SOURCE_ROLE));
				}
			}
			
			new JsonResult("roles", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupDomainRoles", ex);
			new JsonResult(false, "Unable to lookup roles").printTo(out);
		}
	}
	
	public void processLookupDomainUsers(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = new ArrayList<>();
		UserProfile up = getEnv().getProfile();
		
		try {
			boolean wildcard = ServletUtils.getBooleanParameter(request, "wildcard", false);
			boolean fullId = ServletUtils.getBooleanParameter(request, "fullId", false);
			String domainId = ServletUtils.getStringParameter(request, "domainId", null);
			
			List<OUser> users;
			if(RunContext.isSysAdmin()) {
				if(!StringUtils.isEmpty(domainId)) {
					CoreAdminManager coreAdmMgr = getCoreAdminManager();
					users = coreAdmMgr.listUsers(domainId, true);
				} else {
					users = coreMgr.listUsers(true);
				}
			} else { // Domain users can only see users belonging to their own domain
				users = coreMgr.listUsers(true);
			}
			
			if(wildcard) items.add(JsSimple.wildcard(lookupResource(up.getLocale(), CoreLocaleKey.WORD_ALL_MALE)));
			for(OUser user : users) {
				final String id = fullId ? new UserProfileId(user.getDomainId(), user.getUserId()).toString() : user.getUserId();
				items.add(new JsSimple(id, JsSimple.description(user.getDisplayName(), user.getUserId())));
			}
			
			new JsonResult("users", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupUsers", ex);
			new JsonResult(false, "Unable to lookup users").printTo(out);
		}
	}
	
	public void processLookupActivities(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsActivityLkp> items = new ArrayList<>();
		
		try {
			String profileId = ServletUtils.getStringParameter(request, "profileId", true);
			UserProfileId pid = new UserProfileId(profileId);
			
			//TODO: tradurre campo descrizione in base al locale dell'utente
			List<Activity> acts = null;
			if (coreMgr.isTargetProfileEqualTo(pid)) {
				acts = coreMgr.listLiveActivities();
			} else {
				CoreManager mgr = WT.getCoreManager(true, pid);
				acts = mgr.listLiveActivities();
			}
			for(Activity act : acts) {
				items.add(new JsActivityLkp(act));
			}
			new JsonResult(items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupActivities", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLookupCausals(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsCausalLkp> items = new ArrayList<>();
		
		try {
			String profileId = ServletUtils.getStringParameter(request, "profileId", true);
			String masterDataId = ServletUtils.getStringParameter(request, "masterDataId", null);
			UserProfileId pid = new UserProfileId(profileId);
			
			//TODO: tradurre campo descrizione in base al locale dell'utente
			List<Causal> caus = null;
			if (coreMgr.isTargetProfileEqualTo(pid)) {
				caus = coreMgr.listLiveCausals(masterDataId);
			} else {
				CoreManager mgr = WT.getCoreManager(true, pid);
				caus = mgr.listLiveCausals(masterDataId);
			}
			for(Causal cau : caus) {
				items.add(new JsCausalLkp(cau));
			}
			new JsonResult(items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupCausals", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLookupCustomersSuppliers(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsCustomerSupplierLkp> items = new ArrayList<>();
		
		try {
			String query = ServletUtils.getStringParameter(request, "query", "");
			
			final List<String> types = Arrays.asList(
				EnumUtils.toSerializedName(MasterData.Type.CUSTOMER),
				EnumUtils.toSerializedName(MasterData.Type.SUPPLIER)
			);
			for(MasterData entry : coreMgr.listMasterData(types, "%" + query + "%").values()) {
				items.add(new JsCustomerSupplierLkp(entry));
			}
			
			new JsonResult(items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupCustomersSuppliers", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLookupStatisticCustomersSuppliers(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsCustomerSupplierLkp> items = new ArrayList<>();
		
		try {
			String parentMasterDataId = ServletUtils.getStringParameter(request, "parentMasterDataId", null);
			String query = ServletUtils.getStringParameter(request, "query", "");
			
			final List<String> types = Arrays.asList(
				EnumUtils.toSerializedName(MasterData.Type.CUSTOMER),
				EnumUtils.toSerializedName(MasterData.Type.SUPPLIER)
			);
			for(MasterData entry : coreMgr.listChildrenMasterData(parentMasterDataId, types, "%" + query + "%")) {
				final ArrayList<String> tokens = new ArrayList<>(3);
				if (!StringUtils.isEmpty(entry.getAddress())) {
					tokens.add(entry.getAddress());
				}
				if (!StringUtils.isEmpty(entry.getCity())) {
					tokens.add(entry.getCity());
				}
				if (!StringUtils.isEmpty(entry.getCountry())) {
					tokens.add(entry.getCountry());
				}
				final String address = StringUtils.join(tokens, ", ");
				String description = null;
				if (StringUtils.isEmpty(address)) {
					description = entry.getDescription();
				} else {
					description = MessageFormat.format("{0} ({1})", entry.getDescription(), address);
				}
				items.add(new JsCustomerSupplierLkp(entry, description));
			}
			
			new JsonResult(items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupStatisticCustomersSuppliers", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	
	/*
	public void processServerEvents(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<ServiceMessage> messages = new ArrayList();
		
		try {
			messages = ((CorePrivateEnvironment)getEnv()).getSession().getEnqueuedMessages();
			
		} catch (Exception ex) {
			logger.error("Error executing action ServerEvents", ex);
		} finally {
			new JsonResult(JsonResult.gson.toJson(messages)).printTo(out);
		}
	}
	*/
	
	public void processChangeUserPassword(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		try {
			char[] oldPassword = ServletUtils.getStringParameter(request, "oldPassword", true).toCharArray();
			char[] newPassword = ServletUtils.getStringParameter(request, "newPassword", true).toCharArray();
			
			coreMgr.updateUserPassword(oldPassword, newPassword);
			
			new JsonResult().printTo(out);
			
		} catch(Exception ex) {
			logger.error("Error in ChangeUserPassword", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageActivities(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if(crud.equals(Crud.READ)) {
				List<Activity> items =  coreMgr.listAllLiveActivities();
				new JsonResult(items, items.size()).printTo(out);
				
			} else if(crud.equals(Crud.DELETE)) {
				Payload<MapItem, Activity> pl = ServletUtils.getPayload(request, Activity.class);
				coreMgr.deleteActivity(pl.data.getActivityId());
				new JsonResult().printTo(out);
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageCausals", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageActivity(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				Integer id = ServletUtils.getIntParameter(request, "id", null);
				Activity item = coreMgr.getActivity(id);
				new JsonResult(item).printTo(out);
				
			} else if(crud.equals(Crud.CREATE)) {
				Payload<MapItem, Activity> pl = ServletUtils.getPayload(request, Activity.class);
				coreMgr.addActivity(pl.data);
				new JsonResult().printTo(out);
				
			} else if(crud.equals(Crud.UPDATE)) {
				Payload<MapItem, Activity> pl = ServletUtils.getPayload(request, Activity.class);
				coreMgr.updateActivity(pl.data);
				new JsonResult().printTo(out);	
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageCausal", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageCausals(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if(crud.equals(Crud.READ)) {
				List<CausalExt> items =  coreMgr.listAllLiveCausals();
				new JsonResult(items, items.size()).printTo(out);
				
			} else if(crud.equals(Crud.DELETE)) {
				Payload<MapItem, CausalExt> pl = ServletUtils.getPayload(request, CausalExt.class);
				coreMgr.deleteCausal(pl.data.getCausalId());
				new JsonResult().printTo(out);
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageCausals", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageCausal(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if(crud.equals(Crud.READ)) {
				Integer id = ServletUtils.getIntParameter(request, "id", null);
				Causal item = coreMgr.getCausal(id);
				new JsonResult(item).printTo(out);
				
			} else if(crud.equals(Crud.CREATE)) {
				Payload<MapItem, Causal> pl = ServletUtils.getPayload(request, Causal.class);
				coreMgr.addCausal(pl.data);
				new JsonResult().printTo(out);
				
			} else if(crud.equals(Crud.UPDATE)) {
				Payload<MapItem, Causal> pl = ServletUtils.getPayload(request, Causal.class);
				coreMgr.updateCausal(pl.data);
				new JsonResult().printTo(out);	
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageCausal", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	/*
	public void processGetOptionsUsers(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		Connection con = null;
		UserProfile up = getEnv().getProfile();
		
		try {
			ArrayList<JsSimple> data = new ArrayList<>();
			if(up.isWebTopAdmin()) {
				con = WT.getCoreConnection();
				UserDAO udao = UserDAO.getInstance();
				List<OUser> users = udao.selectAll(con);
				String id = null, descr = null;
				for(OUser user : users) {
					id = DomainAccount.buildName(user.getDomainId(), user.getUserId());
					descr = MessageFormat.format("{0} ({1})", user.getDisplayName(), id);
					data.add(new JsSimple(id, descr));
				}
				
			} else {
				//TODO: maybe define a permission to other users to control others options
				data.add(new JsSimple(up.getStringId(), up.getDisplayName()));
			}
			new JsonResult("users", data).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error executing action GetOptionsUsers", ex);
			new JsonResult(false, "Unable to get users").printTo(out);
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	*/
	
	public void processGetOptionsServices(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<UserOptionsServiceData> data = null;
		
		try {
			String id = ServletUtils.getStringParameter(request, "id", true);
			
			UserProfileId targetPid = new UserProfileId(id);
			if(getWts().getProfileId().equals(targetPid)) {
				data = coreMgr.getAllowedUserOptionServices();
			} else {
				CoreManager xcore = WT.getCoreManager(targetPid);
				data = xcore.getAllowedUserOptionServices();
			}
			
			/*
			//TODO: aggiornare l'implementazione
			data.add(new UserOptionsServiceData("com.sonicle.webtop.core", "wt", "WebTop Services", "Sonicle.webtop.core.view.CoreOptions"));
			if(!UserProfile.isWebTopAdmin(id)) data.add(new UserOptionsServiceData("com.sonicle.webtop.calendar", "wtcal", "Calendario", "Sonicle.webtop.calendar.CalendarOptions"));
			if(!UserProfile.isWebTopAdmin(id)) data.add(new UserOptionsServiceData("com.sonicle.webtop.mail", "wtmail", "Posta Elettronica", "Sonicle.webtop.mail.MailOptions"));
			*/
			new JsonResult(data).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in GetOptionsServices", ex);
			new JsonResult(false, "Error in GetOptionsServices").printTo(out);
		}
	}
	
	public void processLookupSessionServices(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		WebTopSession wts = getEnv().getSession();
		Locale locale = wts.getLocale();
		
		ArrayList<JsSimple> items = new ArrayList<>();
		List<String> ids = wts.getPrivateServices();
		for(String id : ids) {
			items.add(new JsSimple(id, WT.lookupResource(id, locale, BaseService.RESOURCE_SERVICE_NAME)));
		}
		new JsonResult("services", items).printTo(out);
	}
	
	public void processFeedback(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			Payload<MapItem, JsFeedback> pl = ServletUtils.getPayload(request, JsFeedback.class);
			
			logger.debug("message: {}", pl.data.message);
			Thread.sleep(4000);
			new JsonResult().printTo(out);
			//new JsonResult(false, "Erroreeeeeeeeeeeeeeeeeeeeeee").printTo(out);
			
		} catch(Exception ex) {
			logger.error("Error in action Feedback", ex);
			new JsonResult(false, "Unable to send feedback report.").printTo(out);
		}
	}
	
	public void processGetWhatsnewTabs(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		WebTopSession wts = ((CorePrivateEnvironment)getEnv()).getSession();
		ArrayList<JsWhatsnewTab> tabs = null;
		JsWhatsnewTab tab = null;
		String html = null;
		UserProfile profile = getEnv().getProfile();
		
		try {
			boolean full = ServletUtils.getBooleanParameter(request, "full", false);
			
			tabs = new ArrayList<>();
			List<String> ids = wts.getPrivateServices();
			for(String id : ids) {
				if(full || wts.needWhatsnew(id, profile)) {
					html = wts.getWhatsnewHtml(id, profile, full);
					if(!StringUtils.isEmpty(html)) {
						tab = new JsWhatsnewTab(id);
						tab.title = WT.lookupResource(id, profile.getLocale(), CoreLocaleKey.SERVICE_NAME);
						tabs.add(tab);
					}
				}
			}
			new JsonResult(tabs).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in GetWhatsnewTabs", ex);
			new JsonResult(false, "Error in GetWhatsnewTabs").printTo(out);
		}
	}
	
	static final String WHATSNEW_STYLES=
		"html * { font-family: Arial !important; }\n"+
		".wt-whatsnew {\n" +
		"	padding: 10px;\n" +
		"	font-family: Helvetica, Arial, sans-serif;\n" +
		"}\n" +
		".wt-whatsnew > div.wt-wntitle:first-child {\n" +
		"	margin-top: 0;\n" +
		"}\n" +
		"div.wt-wntitle {\n" +
		"	margin-top: 1em;\n" +
		"	/*font-size: 220%;*/\n" +
		"	font-size: 150%;\n" +
		"	font-weight: bold;\n" +
		"}\n" +
		"div.wt-wnsubtitle {\n" +
		"	margin-top: 0em;\n" +
		"	/*font-size: 130%;*/\n" +
		"	font-size: 110%;\n" +
		"	font-family: Georgia,\"Times New Roman\",Times,serif;\n" +
		"	font-style: italic;\n" +
		"	color: #797c80;\n" +
		"}\n" +
		"div.wt-wnlist {\n" +
		"	margin-top: 1em;\n" +
		"	/*font-size: 110%;*/\n" +
		"}\n" +
		".wt-wnlist ul {\n" +
		"	padding-left: 30px;\n" +
		"}\n" +
		".wt-wnlist ul li {\n" +
		"	list-style: initial;\n" +
		"}\n" +
		".wt-wnlist ul li h1, .wt-wnlist ul li h2, .wt-wnlist ul li h3 {\n" +
		"	margin-top: .2em;\n" +
		"	font-size: 110%;\n" +
		"	font-weight: bold;\n" +
		"}\n" +
		".wt-wnlist ul li p {\n" +
		"	margin-bottom: .5em;\n" +
		"}\n" +
		".wt-wnlist ul li img {\n" +
		"	padding: .2em;\n" +
		"	margin-bottom: .5em;\n" +
		"	background-color: #fff;\n" +
		"	box-shadow: 2px 2px 5px #e1e1e1;\n" +
		"	border: solid 1px #ccc;\n" +
		"}\n";
	
	public void processGetWhatsnewHTML(HttpServletRequest request, HttpServletResponse response) {
		WebTopSession wts = ((CorePrivateEnvironment)getEnv()).getSession();
		
		try {
			String id = ServletUtils.getStringParameter(request, "id", true);
			boolean full = ServletUtils.getBooleanParameter(request, "full", false);
			String baseUrl=PathUtils.concatPaths(wts.getClientUrl(), "resources/"+id+"/whatsnew/");
			String html = 
					"<html><head><base href=\""+baseUrl+"\"><style>"+WHATSNEW_STYLES+"</style></head><body>"+
					wts.getWhatsnewHtml(id, getEnv().getProfile(), full)+
					"</body><html>";
			ServletUtils.setContentTypeHeader(response, "text/html");
			IOUtils.copy(new StringReader(html), response.getOutputStream());
			
		} catch (Exception ex) {
			logger.error("Error in GetWhatsnewHTML", ex);
		}
	}
	
	public void processTurnOffWhatsnew(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		WebTopSession wts = ((CorePrivateEnvironment)getEnv()).getSession();
		
		try {
			UserProfile profile = getEnv().getProfile();
			List<String> ids = wts.getPrivateServices();
			for(String id : ids) {
				wts.resetWhatsnew(id, profile);
			}
			
		} catch (Exception ex) {
			logger.error("Error in TurnOffWhatsnew", ex);
		} finally {
			new JsonResult().printTo(out);
		}
	}
	
	public void processDownloadAddon(HttpServletRequest request, HttpServletResponse response) {
		try {
			String addonId = ServletUtils.getStringParameter(request, "addonId", true);
			
			if (addonId.equals("notifier")) {
				InputStream is = null;
				try {
					if (!LangUtils.classForNameQuietly("com.sonicle.webtop.addons.AddOns")) {
						throw new WTException("Notifier not available");
					}
					is = this.getClass().getResourceAsStream("/webtop/addons/webtop.exe");
					ServletUtils.setFileStreamHeadersForceDownload(response, "notifier.exe");
					IOUtils.copy(is, response.getOutputStream());
				} finally {
					IOUtils.closeQuietly(is);
				}
				
			} else {
				throw new WTException("Unknown addon [{0}]", addonId);
			}
			
		} catch(Exception ex) {
			logger.error("Error in action DownloadFiles", ex);
			ServletUtils.writeError(response, HttpServletResponse.SC_NOT_FOUND);
		}
	}
	
	public void processPbxCall(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		String number = ServletUtils.getStringParameter(request, "number", null);

		if (coreMgr.pbxConfigured() && number!=null) {
			try {
				coreMgr.pbxCall(number);
				new JsonResult().printTo(out);
			} catch(WTException exc) {
				logger.error("Error during PBX Call",exc);
				new JsonResult(false,exc.getMessage()).printTo(out);
			}
		} else {
			new JsonResult(false,"No PBX Configured").printTo(out);
		}
			
	}
	
	public void processSnoozeReminder(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			Integer snooze = ServletUtils.getIntParameter(request, "snooze", 5);
			PayloadAsList<JsReminderInApp.List> pl = ServletUtils.getPayloadAsList(request, JsReminderInApp.List.class);
			
			DateTime remindOn = DateTimeUtils.now(false).plusMinutes(snooze);
			for(JsReminderInApp js : pl.data) {
				coreMgr.snoozeReminder(JsReminderInApp.createReminderInApp(getEnv().getProfileId(), js), remindOn);
			}
			new JsonResult().printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in SnoozeReminder", ex);
			new JsonResult(false, "Error in SnoozeReminder").printTo(out);
		}
	}
	
	public void processManageOTP(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		WebTopSession wts = ((CorePrivateEnvironment)getEnv()).getSession();
		UserProfileId pid = getEnv().getProfile().getId();
		CoreManager corem = null;
		
		try {
			String operation = ServletUtils.getStringParameter(request, "operation", true);
			if (operation.equals("configure") || operation.equals("activate") || operation.equals("deactivate")) {
				// These work only on a target user!
				String profileId = ServletUtils.getStringParameter(request, "profileId", true);
				
				UserProfileId targetPid = new UserProfileId(profileId);
				corem = (targetPid.equals(coreMgr.getTargetProfileId())) ? coreMgr : WT.getCoreManager(targetPid);
				
				if (operation.equals("configure")) {
					OtpDeliveryMode deliveryMode = ServletUtils.getEnumParameter(request, "delivery", true, OtpDeliveryMode.class);
					if (OtpDeliveryMode.EMAIL.equals(deliveryMode)) {
						String address = ServletUtils.getStringParameter(request, "address", true);
						InternetAddress ia = InternetAddressUtils.toInternetAddress(address, null);
						if (!InternetAddressUtils.isAddressValid(ia)) throw new WTException("Email address not valid"); //TODO: messaggio in lingua
						
						OTPManager.EmailConfig config = corem.otpConfigureUsingEmail(address);
						wts.setProperty(SERVICE_ID, WTSPROP_OTP_SETUP, config);

					} else if (OtpDeliveryMode.GOOGLEAUTH.equals(deliveryMode)) {
						OTPManager.GoogleAuthConfig config = corem.otpConfigureUsingGoogleAuth(200);
						wts.setProperty(SERVICE_ID, WTSPROP_OTP_SETUP, config);
					}
					new JsonResult(true).printTo(out);
					
				} else if (operation.equals("activate")) {
					int code = ServletUtils.getIntParameter(request, "code", true);

					OTPManager.Config config = (OTPManager.Config)wts.getProperty(SERVICE_ID, WTSPROP_OTP_SETUP);
					boolean enabled = corem.otpActivate(config, code);
					/*
					if (!enabled) {
						throw new WTLocalizedException(lookupResource(CoreLocaleKey.OTP_SETUP_ERROR_CODE), "Invalid code");
					}
					*/
					if (!enabled) throw new WTException("Invalid code"); //TODO: messaggio in lingua
					wts.clearProperty(SERVICE_ID, WTSPROP_OTP_SETUP);
					
					new JsonResult().printTo(out);

				} else if (operation.equals("deactivate")) {
					corem.otpDeactivate();
					new JsonResult().printTo(out);

				}
			} else if (operation.equals("untrustthis")) {
				// This works only on current session user!
				OTPManager otpm = coreMgr.getOTPManager();
				TrustedDeviceCookie tdc = otpm.readTrustedDeviceCookie(pid, request);
				if (tdc != null) {
					otpm.removeTrustedDevice(pid, tdc.deviceId);
					otpm.clearTrustedDeviceCookie(pid, response);
				}
				new JsonResult().printTo(out);
				
			} else if (operation.equals("untrustothers")) {
				// This works only on current session user!
				OTPManager otpm = coreMgr.getOTPManager();
				TrustedDeviceCookie thistdc = otpm.readTrustedDeviceCookie(pid, request);
				List<JsTrustedDevice> tds = otpm.listTrustedDevices(pid);
				for(JsTrustedDevice td: tds) {
					if ((thistdc != null) && (td.deviceId.equals(thistdc.deviceId))) continue;
					otpm.removeTrustedDevice(pid, td.deviceId);
				}
				new JsonResult().printTo(out);
			}
			
		} catch (Exception ex) {
			logger.error("Error in ManageOTP", ex);
			new JsonResult(false, "Error in ManageOTP").printTo(out);
		}
	}
	
	public void processCleanupDocManagerEditing(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String editingId = ServletUtils.getStringParameter(request, "editingId", true);
			getWts().finalizeDocumentEditing(editingId);
			new JsonResult().printTo(out);
			
		} catch(ParameterException ex) {
			logger.error("Error in CleanupDocManagerEditing", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	/*
	private void sendOtpCodeEmail(UserProfileId pid, Locale locale, InternetAddress to, String verificationCode) {
		try {
			String bodyHeader = WT.lookupResource(SERVICE_ID, locale, CoreLocaleKey.TPL_EMAIL_OTPCODEVERIFICATION_BODY_HEADER);
			String subject = NotificationHelper.buildSubject(locale, SERVICE_ID, bodyHeader);
			String html = TplHelper.buildOtpCodeVerificationEmail(locale, verificationCode);
			
			InternetAddress from = WT.getNotificationAddress(pid.getDomainId());
			if(from == null) throw new WTException("Error building sender address");
			WT.sendEmail(WT.getGlobalMailSession(pid), true, from, to, subject, html);

		} catch(IOException | TemplateException ex) {
			logger.error("Unable to build email template", ex);
		} catch(Exception ex) {
			logger.error("Unable to send email", ex);
		}
	}
	*/
	
	/*
	private void sendOtpCodeEmail2(OSharingLink olink, String path, String ipAddress, String userAgent) throws WTException {
		final String BHD_KEY = (olink.getLinkType().equals(SharingLink.TYPE_DOWNLOAD)) ? VfsLocale.TPL_EMAIL_SHARINGLINKUSAGE_BODY_HEADER_DL : VfsLocale.TPL_EMAIL_SHARINGLINKUSAGE_BODY_HEADER_UL;
		UserProfileId pid = olink.getProfileId();
		
		//TODO: rendere relativa la path del file rispetto allo Store???
		try {
			UserProfile.Data ud = WT.getUserData(olink.getProfileId());
			String bodyHeader = lookupResource(ud.getLocale(), BHD_KEY);
			String source = NotificationHelper.buildSource(ud.getLocale(), SERVICE_ID);
			String subject = TplHelper.buildLinkUsageEmailSubject(ud.getLocale(), bodyHeader);
			String customBody = TplHelper.buildLinkUsageBodyTpl(ud.getLocale(), olink.getSharingLinkId(), PathUtils.getFileName(olink.getFilePath()), path, ipAddress, userAgent);
			String html = NotificationHelper.buildCustomBodyTplForNoReplay(ud.getLocale(), source, bodyHeader, customBody);

			//InternetAddress from = WT.buildDomainInternetAddress(pid.getDomainId(), "webtop-notification", null);
			//if(from == null) throw new WTException("Error building sender address");
			InternetAddress from = WT.getNotificationAddress(pid.getDomainId());
			InternetAddress to = ud.getEmail();
			if(to == null) throw new WTException("Error building destination address");
			WT.sendEmail(getMailSession(), true, from, to, subject, html);

		} catch(IOException | TemplateException ex) {
			logger.error("Unable to build email template", ex);
		} catch(Exception ex) {
			logger.error("Unable to send email", ex);
		}
	}
	*/
	
	public void processGetOTPGoogleAuthQRCode(HttpServletRequest request, HttpServletResponse response) {
		WebTopSession wts = ((CorePrivateEnvironment)getEnv()).getSession();
		
		try {
			OTPManager.GoogleAuthConfig config = (OTPManager.GoogleAuthConfig)wts.getProperty(SERVICE_ID, WTSPROP_OTP_SETUP);
			ServletUtils.writeContent(response, config.qrcode, config.qrcode.length, "image/png");
			
		} catch (Exception ex) {
			logger.error("Error in GetOTPGoogleAuthQRCode", ex);
		}
	}
		
	public void processListInternetRecipientsSources(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		ArrayList<JsSimple> items = new ArrayList<>();
		
		try {
			for(String soid : coreMgr.listRecipientProviderSourceIds()) {
				RecipientsProviderBase provider = coreMgr.getProfileRecipientsProvider(soid);
				items.add(new JsSimple(soid, provider.getDescription()));
			}
			new JsonResult("sources", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in ListInternetRecipientsSource", ex);
			new JsonResult(false, "Error in ListInternetRecipientsSources").printTo(out);
		}
	}
	
	public void processManageInternetRecipients(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<Recipient> items = null;
		
		try {
			ArrayList<String> sources = ServletUtils.getStringParameters(request, "sources");
			String crud = ServletUtils.getStringParameter(request, "crud", Crud.READ);
			String query = ServletUtils.getStringParameter(request, "query", "");
			boolean builtInAtTheEnd = ServletUtils.getBooleanParameter(request, "autoLast", false);
			RecipientFieldType rft = ServletUtils.getEnumParameter(request, "rftype", RecipientFieldType.EMAIL, RecipientFieldType.class);
			if (crud.equals(Crud.READ)) {
				int limit = ServletUtils.getIntParameter(request, "limit", 100);
				if (limit==0) limit=Integer.MAX_VALUE;

				if (sources.isEmpty()) {
					items = coreMgr.listProviderRecipients(rft, query, limit, builtInAtTheEnd);
				} else {
					items = coreMgr.listProviderRecipients(rft, sources, query, limit);
				}
				
				if (rft.equals(RecipientFieldType.FAX)) {
					//compile fax email patterns
					for(Recipient r: items) {
						r.setAddress(compileFaxPattern(r));
					}
				}
				
				new JsonResult("recipients", items, items.size()).printTo(out);
				
			} else if (crud.equals(Crud.DELETE)) {
				Payload<MapItem, JsInternetAddress> pl = ServletUtils.getPayload(request, JsInternetAddress.class);
				if (CoreManager.RECIPIENT_PROVIDER_AUTO_SOURCE_ID.equals(pl.data.source)) {
					String fullAddress = InternetAddressUtils.toFullAddress(pl.data.address, pl.data.personal);
					if (fullAddress != null) {
						coreMgr.deleteServiceStoreEntry(SERVICE_ID, "recipients", fullAddress.toUpperCase());
					}
					new JsonResult().printTo(out);
					
				} else {
					throw new Exception("Cannot delete from source "+pl.data.source+" ["+pl.data.sourceName+"]");
				}
			}

		} catch (Exception ex) {
			logger.error("Error in ManageInternetRecipients", ex);
			new JsonResult(false, "Error in ManageInternetRecipients").printTo(out);
		}
	}
	
	private String compileFaxPattern(Recipient r) {
		String faxAddress=r.getAddress();
		//fix fax number with "+"
        StringBuffer newfax=new StringBuffer();
        for(char c: faxAddress.toCharArray()) {
            if (Character.isDigit(c)) newfax.append(c);
            else if (c=='+') {
                if (newfax.length()==0) newfax.append("00");
            }
        }
		String username=r.getPersonal().toLowerCase().replaceAll(" ", ".");
		String faxPattern=ss.getFaxPattern();
		return faxPattern.replace("{username}", username).replace("{number}", newfax.toString());
	}
	
	public void processListDomainPublicImages(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		try {
			String domainId = ServletUtils.getStringParameter(request, "domainId", null);
			
			List<PublicImage> images;
			if (RunContext.isSysAdmin()) {
				if (StringUtils.isBlank(domainId)) throw new WTException();
				CoreAdminManager coreAdmMgr = getCoreAdminManager();
				images = coreAdmMgr.listDomainPublicImages(domainId);
				
			} else { // Domain users can only use images belonging to their own domain
				images = coreMgr.listDomainPublicImages();
			}
			
			ArrayList<JsPublicImage> items = new ArrayList<>(images.size());
			int i = 0;
			for (PublicImage image : images) {
				items.add(new JsPublicImage("img" + i, image));
				i++;
			}
			new JsonResult("images", items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in ListDomainPublicImages", ex);
			new JsonResult(false, "Error in ListDomainPublicImages").printTo(out);
		}
	}
	
	public void processRestoreAutosave(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		Connection con=null;
		try {
			con=getConnection();
			boolean mine=ServletUtils.getBooleanParameter(request, "mine", true);
			String cid = getWts().getClientTrackingID();
			
			List<OAutosave> items = (mine) ? coreMgr.listMyAutosaveData(cid) : coreMgr.listOfflineOthersAutosaveData(cid);
			for(OAutosave item: items) {
				getWts().notify(new ServiceMessage(item.getServiceId(),"autosaveRestore",
						new JsAutosave(
								item.getDomainId(),
								item.getUserId(),
								item.getWebtopClientId(),
								item.getServiceId(),
								item.getContext(),
								item.getKey(),
								item.getValue()
						)
				));
			}
			new JsonResult().printTo(out);
		} catch (Exception ex) {
			logger.error("Error in processRestoreAutosave", ex);
			new JsonResult(false, "Error in processRestoreAutosave").printTo(out);
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	public void processSendSMS(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		try {
			String number=ServletUtils.getStringParameter(request, "number", true);
			String text=ServletUtils.getStringParameter(request, "text", true);
			coreMgr.smsSend(number,text);
			new JsonResult().printTo(out);
		} catch (Exception ex) {
			logger.error("Error in processSendSMS", ex);
			new JsonResult(false, ex.getMessage()).printTo(out);
		}
	}
	
	public void processRemoveAutosave(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		Connection con=null;
		try {
			con=getConnection();
			boolean allOthers=ServletUtils.getBooleanParameter(request, "allOthers", false);
			String cid = getWts().getClientTrackingID();
			
			if (allOthers) {
				coreMgr.deleteOfflineOthersAutosaveData(cid);
			} else {
				boolean allMine=ServletUtils.getBooleanParameter(request, "allMine", false);
				if (allMine) {
					coreMgr.deleteMyAutosaveData(cid);
				} else {
					String serviceId=ServletUtils.getStringParameter(request, "serviceId", true);
					String context=ServletUtils.getStringParameter(request, "context", true);
					String key=ServletUtils.getStringParameter(request, "key", true);
					String webtopClientId=ServletUtils.getStringParameter(request, "webtopClientId", true);
					
					coreMgr.deleteMyAutosaveData(webtopClientId, serviceId, context, key);
				}
			}
			new JsonResult().printTo(out);
		} catch (Exception ex) {
			logger.error("Error in processRestoreAutosave", ex);
			new JsonResult(false, "Error in processRestoreAutosave").printTo(out);
		} finally {
			DbUtils.closeQuietly(con);
		}
	}
	
	/*
	private List<String> queryDomains() {
		List<String> domains = new ArrayList<>();
		if(RunContext.isWebTopAdmin()) domains.add("*");
		domains.add(getWts().getProfileId().getDomainId());
		return domains;
	}
	*/
	
	public void processManageIM(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals("init")) {
				if (xmppCli != null) {
					PresenceStatus ps = us.getIMPresenceStatus();
					String statusMessage = "Hey there! I'm on WebTop";
							
					try {
						xmppCli.updatePresence(ps, statusMessage);
						
					} catch(XMPPClientException ex1) {
						if (!xmppCli.isConnected()) throw new WTException(ex1, lookupResource(CoreLocaleKey.XMPP_ERROR_CONNECTION));
						if (!xmppCli.isAuthenticated()) throw new WTException(ex1, lookupResource(CoreLocaleKey.XMPP_ERROR_AUTHENTICATION));
						throw ex1;
					}
					Principal p = getEnv().getProfile().getPrincipal();
					new JsonResult(new JsIMInit(ps, statusMessage, p.getUserId()+"@"+p.getAuthenticationDomain().getInternetName(), xmppCli.getUserJid().toString(), p.getPassword())).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
				
			} else if (crud.equals("presence")) {
				String presenceStatus = ServletUtils.getStringParameter(request, "presenceStatus", null);
				String statusMessage = ServletUtils.getStringParameter(request, "statusMessage", null);
				
				PresenceStatus ps = EnumUtils.forSerializedName(presenceStatus, PresenceStatus.class);
				if (ps == null) ps = PresenceStatus.ONLINE;
				if (statusMessage == null) statusMessage = "Hey there! I'm on WebTop";

				if (xmppCli != null) {
					xmppCli.updatePresence(ps, statusMessage);
					us.setIMPresenceStatus(ps);
					us.setIMStatusMessage(statusMessage);
					new JsonResult().printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageIM", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processLookupIMFriends(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		List<JsSimple> items = new ArrayList<>();
		
		try {
			if (xmppCli != null) {
				for(Friend friend : xmppCli.listFriends()) {
					items.add(new JsSimple(friend.getId(), friend.getNickame()));
				}
			}
			new JsonResult(items, items.size()).printTo(out);
			
		} catch (Exception ex) {
			logger.error("Error in LookupIMFriends", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageGridIMFriends(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				List<JsGridIMFriend> items = new ArrayList<>();
				if (xmppCli != null) {
					for(Friend friend : xmppCli.listFriends()) {
						items.add(new JsGridIMFriend(friend, friend.getInstantChatJid()));
					}
				}	
				new JsonResult(items, items.size()).printTo(out);
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageGridIMFriends", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageGridIMChats(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				List<JsGridIMChat> items = new ArrayList<>();
				
				/*
				if (xmppCli != null) {
					for(ChatRoom chat : xmppCli.listChats()) {
						items.add(new JsGridIMChat(chat));
					}
				} else {
					for(IMChat chat : coreMgr.listIMChats(true)) {
						items.add(new JsGridIMChat(chat));
					}
				}
				*/
				for(IMChat chat : coreMgr.listIMChats(false)) {
					items.add(new JsGridIMChat(chat));
				}
					
				new JsonResult(items, items.size()).printTo(out);
				
			} else if(crud.equals(Crud.DELETE)) {
				Payload<MapItem, JsGridIMChat> pl = ServletUtils.getPayload(request, JsGridIMChat.class);
				
				if (xmppCli != null) {
					xmppCli.forgetChat(pl.data.id);
				}
				coreMgr.deleteIMChat(pl.data.id);
				
				final IVfsManager vfsMgr = getVfsManager();
				if (vfsMgr != null) {
					final String path = "/" + WEBCHAT_VFS_FOLDER + "/" + pl.data.id + "/";
					int myDocsStoreId = vfsMgr.getMyDocumentsStoreId();
					
					vfsMgr.deleteStoreFile(myDocsStoreId, path);
				}
				
				new JsonResult().printTo(out);
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageGridIMChats", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageIMChat(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		UserProfile up = getEnv().getProfile();
		DateTimeZone utz = up.getTimeZone();
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals("presence")) {
				String chatId = ServletUtils.getStringParameter(request, "chatId", true);
				
				if (xmppCli != null) {
					if (!XMPPClient.isInstantChat(chatId)) throw new WTException("Presence feature non available for a grupchat");
					
					String friendFullId = null;
					String presenceStatus = null;
					FriendPresence presence = xmppCli.getChatPresence(chatId);
					if (presence == null) {
						presenceStatus = EnumUtils.toSerializedName(PresenceStatus.OFFLINE);
					} else {
						friendFullId = presence.getFriendFullJid();
						presenceStatus = EnumUtils.toSerializedName(presence.getPresenceStatus());
					}
					new JsonResult(new JsIMPresenceStatus(friendFullId, presenceStatus)).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
				
			} else if (crud.equals("dates")) {
				String chatId = ServletUtils.getStringParameter(request, "chatId", true);
				int year = ServletUtils.getIntParameter(request, "year", true);
				
				List<String> items = new ArrayList<>();
				List<LocalDate> dates = coreMgr.listIMMessageDates(chatId, year, utz);
				for(LocalDate date : dates) {
					items.add(date.toString());
				}
				
				new JsonResult(items).printTo(out);
				
			} else if (crud.equals("send")) {
				String chatId = ServletUtils.getStringParameter(request, "chatId", true);
				String text = ServletUtils.getStringParameter(request, "text", true);
				String lastSeenDate = ServletUtils.getStringParameter(request, "lastSeenDate", null);
				
				LocalDate lastSeen = (lastSeenDate == null) ? null : DateTimeUtils.parseYmdHmsWithZone(lastSeenDate, "00:00:00", utz).toLocalDate();
				
				if (xmppCli != null) {
					EntityBareJid chatJid = XMPPHelper.asEntityBareJid(chatId);
					List<JsGridIMMessage> items = new ArrayList<>();
					
					ChatMessage message = xmppCli.sendTextMessage(chatJid, text);
					if (message == null) throw new Exception("Message is null");
					
					if (lastSeen != null) {
						final LocalDate mesDate = message.getTimestampDate(utz);
						if (!mesDate.equals(lastSeen)) {
							final String msgId = ChatMessage.buildUniqueId(chatJid, "dummy-date", message.getTimestamp());
							items.add(JsGridIMMessage.asDateAction(msgId, mesDate));
						}
					}
					items.add(new JsGridIMMessage(true, message, getEnv().getProfile().getTimeZone()));
					
					new JsonResult(items, items.size()).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
				
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageIMChat", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageGridIMChatMessages(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		UserProfile up = getEnv().getProfile();
		DateTimeZone utz = up.getTimeZone();
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				String chatId = ServletUtils.getStringParameter(request, "chatId", true);
				String date = ServletUtils.getStringParameter(request, "date", null);
				
				final LocalDate nowLd = DateTime.now().withZone(utz).toLocalDate();
				LocalDate ld = (date == null) ? nowLd : DateTimeUtils.parseYmdHmsWithZone(date, "00:00:00", utz).toLocalDate();
				boolean history = (ld.compareTo(nowLd) != 0);
				
				IMChat chat = coreMgr.getIMChat(chatId);
				if (xmppCli != null) {
					List<JsGridIMMessage> items = new ArrayList<>();
					EntityBareJid chatJid = XMPPHelper.asEntityBareJid(chatId);
					EntityBareJid myJid = xmppCli.getUserJid().asEntityBareJid();
					
					final DateTime messageTs = ChatMessage.nowTimestamp();
					LocalDate lastDate = null;
					
					HashMap<String, String> cacheNicks = new HashMap<>();
					cacheNicks.put(myJid.toString(), xmppCli.getUserNickame()); // Fill cache with my data
					List<IMMessage> messages = coreMgr.listIMMessages(chatId, ld, utz, !history);
					
					// Add unavailable warning at the beginning
					if (history && !messages.isEmpty()) {
						if (chat != null && chat.isUnavailable()) {
							final String msgId = ChatMessage.buildUniqueId(chatJid, "dummy-unavailable1", messageTs);
							items.add(JsGridIMMessage.asWarnAction(msgId, messageTs, "unavailable"));
						}
					}
					
					for(IMMessage mes : messages) {
						final LocalDate mesDate = mes.getTimestampDate(utz);
						if (!mesDate.equals(lastDate)) {
							lastDate = mesDate;
							final String msgId = ChatMessage.buildUniqueId(chatJid, "dummy-date", mes.getTimestamp());
							items.add(JsGridIMMessage.asDateAction(msgId, lastDate));
						}
						
						if (!cacheNicks.containsKey(mes.getSenderJid())) {
							if (xmppCli.isAuthenticated()) {
								cacheNicks.put(mes.getSenderJid(), xmppCli.getFriendNickname(XMPPHelper.asEntityBareJid(mes.getSenderJid())));
							} else {
								cacheNicks.put(mes.getSenderJid(), XMPPHelper.buildGuessedString(mes.getSenderJid()));
							}
						}
						final String nick = cacheNicks.get(mes.getSenderJid());
						items.add(new JsGridIMMessage(myJid.equals(mes.getSenderJid()), mes, nick, utz));
					}
					
					// Add unavailable warning at the end
					if (chat != null && chat.isUnavailable()) {
						final String msgId = ChatMessage.buildUniqueId(chatJid, "dummy-unavailable2", messageTs);
						items.add(JsGridIMMessage.asWarnAction(msgId, messageTs, "unavailable"));
					}
					
					new JsonResult(items, items.size()).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageGridIMChatMessages", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageGridIMChatSearch(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		UserProfile up = getEnv().getProfile();
		DateTimeZone utz = up.getTimeZone();
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				String chatId = ServletUtils.getStringParameter(request, "chatId", true);
				String query = ServletUtils.getStringParameter(request, "query", null);
				
				if (xmppCli != null) {
					List<JsGridIMChatSearch> items = new ArrayList<>();
					EntityBareJid myJid = xmppCli.getUserJid().asEntityBareJid();
					
					if (query != null) {
						HashMap<String, String> cacheNicks = new HashMap<>();
						cacheNicks.put(myJid.toString(), xmppCli.getUserNickame()); // Fill cache with my data
						List<IMMessage> messages = coreMgr.findIMMessagesByQuery(chatId, "%"+query+"%", utz);
						
						for(IMMessage mes : messages) {
							if (!cacheNicks.containsKey(mes.getSenderJid())) {
								if (xmppCli.isAuthenticated()) {
									cacheNicks.put(mes.getSenderJid(), xmppCli.getFriendNickname(XMPPHelper.asEntityBareJid(mes.getSenderJid())));
								} else {
									cacheNicks.put(mes.getSenderJid(), XMPPHelper.buildGuessedString(mes.getSenderJid()));
								}
							}
							final String nick = cacheNicks.get(mes.getSenderJid());
							items.add(new JsGridIMChatSearch(mes, nick, utz));
						}
					}
					
					new JsonResult(items, items.size()).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageGridIMChatSearch", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	public void processManageGroupChat(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
		
		try {
			String crud = ServletUtils.getStringParameter(request, "crud", true);
			if (crud.equals(Crud.READ)) {
				String id = ServletUtils.getStringParameter(request, "id", null);
				
				ChatRoom chat = xmppCli.getChat(id);
				List<ChatMember> members = xmppCli.getChatMembers(id);
				new JsonResult(new JsGroupChat((GroupChatRoom)chat, members)).printTo(out);
				
			} else if(crud.equals(Crud.CREATE)) {
				Payload<MapItem, JsGroupChat> pl = ServletUtils.getPayload(request, JsGroupChat.class);
				
				if (xmppCli != null) {
					ArrayList<EntityBareJid> withUsers = new ArrayList<>();
					for(JsGroupChat.Member member : pl.data.members) {
						withUsers.add(XMPPHelper.asEntityBareJid(member.friendId));
					}
					EntityBareJid chatId = xmppCli.newGroupChat(pl.data.name, withUsers);
					pl.data.id = chatId.asEntityBareJidString();
					
					new JsonResult(pl.data).printTo(out);
					
				} else {
					throw new WTException("XMPPClient not available");
				}
				
			} else if(crud.equals(Crud.UPDATE)) {
				Payload<MapItem, JsGroupChat> pl = ServletUtils.getPayload(request, JsGroupChat.class);
				
				new JsonResult().printTo(out);
			}
			
		} catch(Exception ex) {
			logger.error("Error in ManageGroupChat", ex);
			new JsonResult(ex).printTo(out);
		}
	}
	
	private GroupChatRoom createGroupChatRoom(IMChat chat) {
		final EntityBareJid chatJid = XMPPHelper.asEntityBareJid(chat.getChatJid());
		final EntityBareJid ownerJid = XMPPHelper.asEntityBareJid(chat.getOwnerJid());
		return new GroupChatRoom(chatJid, ownerJid, chat.getName(), chat.getLastSeenActivity());
	}
	
	private InstantChatRoom createDirectChatRoom(IMChat chat) {
		final EntityBareJid chatJid = XMPPHelper.asEntityBareJid(chat.getChatJid());
		final EntityBareJid ownerJid = XMPPHelper.asEntityBareJid(chat.getOwnerJid());
		final EntityBareJid withJid = XMPPHelper.asEntityBareJid(chat.getWithJid());
		return new InstantChatRoom(chatJid, ownerJid, chat.getName(), chat.getLastSeenActivity(), withJid);
	}
	
	private IMMessage createIMMessage(EntityBareJid chatJid, ChatMessage message) {
		IMMessage mes = new IMMessage();
		mes.setChatJid(chatJid.toString());
		mes.setSenderJid(message.getFromUser().toString());
		mes.setSenderResource(message.getFromUserResource());
		mes.setTimestamp(message.getTimestamp());
		mes.setDeliveryTimestamp(message.getDeliveryTimestamp());
		mes.setAction(IMMessage.Action.NONE);
		mes.setText(message.getText());
		mes.setData(null);
		OutOfBandData oob = message.getOutOfBandExtension();
		if (oob != null) {
			mes.setAction(IMMessage.Action.FILE);
			mes.setData(JsGridIMMessage.toData(message.getText(), oob));
		}
		mes.setMessageUid(message.getMessageUid());
		mes.setStanzaId(message.getStanzaId());
		return mes;
	}
	
	private IMMessage createIMMessage(EntityBareJid chatJid, EntityBareJid senderJid, DateTime timestamp, DateTime deliveryTimestamp, IMMessage.Action action, String text) {
		IMMessage mes = new IMMessage();
		mes.setChatJid(chatJid.toString());
		mes.setSenderJid(senderJid.toString());
		mes.setSenderResource(null);
		mes.setTimestamp(timestamp);
		mes.setDeliveryTimestamp(deliveryTimestamp);
		mes.setAction(action);
		mes.setText(text);
		mes.setData(null);
		mes.setMessageUid(ChatMessage.buildUniqueId(senderJid, timestamp));
		mes.setStanzaId(null);
		return mes;
	}
	
	private class XMPPServiceListenerImpl implements XMPPClientListener {
		
		@Override
		public void onFriendsAdded(Collection<Jid> jids) {
			getWts().notify(new IMFriendsUpdated());
		}

		@Override
		public void onFriendsUpdated(Collection<Jid> jids) {
			getWts().notify(new IMFriendsUpdated());
		}

		@Override
		public void onFriendsDeleted(Collection<Jid> jids) {
			getWts().notify(new IMFriendsUpdated());
		}
		
		@Override
		public void onFriendPresenceChanged(Jid jid, FriendPresence presence, FriendPresence bestPresence) {
			logger.debug("presenceChanged {}", jid.toString());
			final EntityBareJid targetBareJid = jid.asEntityBareJidIfPossible();
			final String presenceStatus = EnumUtils.toSerializedName(bestPresence.getPresenceStatus());
			getWts().notify(new IMFriendPresenceUpdated(targetBareJid.toString(), bestPresence.getFriendFullJid(), bestPresence.getInstantChatBareJid(), presenceStatus, bestPresence.getStatusMessage()));
		}
		
		@Override
		public void onChatRoomUpdated(ChatRoom chatRoom, boolean self) {
			getWts().notify(new IMChatRoomUpdated(chatRoom.getChatJid().toString(), chatRoom.getName()));
		}
		
		@Override
		public void onChatRoomAdded(ChatRoom chatRoom, String ownerNick, boolean self) {
			if (chatRoom instanceof InstantChatRoom) {
				InstantChatRoom dcr = (InstantChatRoom)chatRoom;
				logger.trace("Adding direct chat room [{}, {}]", dcr.getChatJid().toString(), dcr.getOwnerJid().toString());
				
				try {
					IMChat chat = new IMChat();
					chat.setChatJid(dcr.getChatJid().toString());
					chat.setOwnerJid(dcr.getOwnerJid().toString());
					chat.setName(dcr.getName());
					chat.setIsGroupChat(false);
					chat.setWithJid(dcr.getWithJid().toString());
					coreMgr.addIMChat(chat);
					
				} catch(WTException ex) {
					logger.error("Error saving direct chat [{}]", ex, dcr.getChatJid().toString());
				}
				
			} else if (chatRoom instanceof GroupChatRoom) {
				GroupChatRoom gcr = (GroupChatRoom)chatRoom;
				logger.trace("Adding group chat room [{}, {}]", gcr.getChatJid().toString(), gcr.getOwnerJid().toString());
				
				try {
					IMChat chat = new IMChat();
					chat.setChatJid(gcr.getChatJid().toString());
					chat.setOwnerJid(gcr.getOwnerJid().toString());
					chat.setName(gcr.getName());
					chat.setIsGroupChat(true);
					coreMgr.addIMChat(chat);
					
				} catch(WTException ex) {
					logger.error("Error saving group chat [{}]", ex, gcr.getChatJid().toString());
				}
			}
			getWts().notify(new IMChatRoomAdded(chatRoom.getChatJid().toString(), chatRoom.getName(), chatRoom.getOwnerJid().asEntityBareJidString(), ownerNick, self));
		}
		
		@Override
		public void onChatRoomRemoved(EntityBareJid chatJid, String chatName, EntityBareJid ownerJid, String ownerNick) {
			try {
				coreMgr.deleteIMChat(chatJid.asEntityBareJidString());

			} catch(WTException ex) {
				logger.error("Error deleting group chat [{}]", ex, chatJid.toString());
			}
			getWts().notify(new IMChatRoomRemoved(chatJid.toString(), chatName, ownerJid.asEntityBareJidString(), ownerNick));
		}
		
		@Override
		public void onChatRoomUnavailable(ChatRoom chatRoom, String ownerNick) {
			//DateTimeZone utz = getEnv().getProfile().getTimeZone();
			
			try {
				coreMgr.updateIMChatAvailablity(chatRoom.getChatJid().asEntityBareJidString(), false);
				IMMessage mes = createIMMessage(chatRoom.getChatJid(), chatRoom.getOwnerJid(), chatRoom.getLastSeenActivity(), null, IMMessage.Action.CHAT_CLOSE, null);
				coreMgr.addIMMessage(mes);

			} catch(WTException ex) {
				logger.error("Error deleting group chat [{}]", ex, chatRoom.getChatJid().toString());
			}
			
			getWts().notify(new IMChatRoomClosed(chatRoom.getChatJid().toString(), chatRoom.getName(), chatRoom.getOwnerJid().asEntityBareJidString(), ownerNick));
		}
		
		@Override
		public void onChatRoomMessageSent(ChatRoom chatRoom, ChatMessage message) {
			//DateTimeZone utz = getEnv().getProfile().getTimeZone();
			logger.trace("Message sent {}, {}, {}", chatRoom.getChatJid().toString(), message.getStanzaId(), message.getRawMessage().getBody());
			
			try {
				IMMessage mes = createIMMessage(chatRoom.getChatJid(), message);
				coreMgr.addIMMessage(mes);
				coreMgr.updateIMChatLastSeenActivity(chatRoom.getChatJid().toString(), chatRoom.getLastSeenActivity());
				
			} catch(WTException ex) {
				logger.error("Error saving chat message [{}, {}]", ex, chatRoom.getChatJid().toString(), message.getMessageUid());
			}
		}
		
		@Override
		public void onChatRoomMessageReceived(ChatRoom chatRoom, ChatMessage message) {
			DateTimeZone utz = getEnv().getProfile().getTimeZone();
			DateTimeFormatter fmt = DateTimeUtils.createYmdHmsFormatter(utz);
			
			String action = EnumUtils.toSerializedName(IMMessage.Action.NONE);
			String data = null;
			OutOfBandData oob = message.getOutOfBandExtension();
			if (oob != null) {
				action = EnumUtils.toSerializedName(IMMessage.Action.FILE);
				data = JsGridIMMessage.toData(message.getText(), oob);
			}
			getWts().notify(new IMChatRoomMessageReceived(chatRoom.getChatJid().toString(), chatRoom.getName(), message.getFromUser().toString(), message.getFromUserNickname(), fmt.print(message.getTimestamp()), message.getMessageUid(), action, message.getText(), data));
			
			try {
				IMMessage mes = createIMMessage(chatRoom.getChatJid(), message);
				coreMgr.addIMMessage(mes);
				coreMgr.updateIMChatLastSeenActivity(chatRoom.getChatJid().toString(), chatRoom.getLastSeenActivity());

			} catch(WTException ex) {
				logger.error("Error saving chat message [{}, {}]", ex, chatRoom.getChatJid().toString(), message.getMessageUid());
			}
		}

		@Override
		public void onChatRoomParticipantJoined(ChatRoom chatRoom, EntityFullJid participant) {
			logger.debug("{} joins group chat {}", participant.toString(), chatRoom.getChatJid().toString());
		}

		@Override
		public void onChatRoomParticipantLeft(ChatRoom chatRoom, EntityFullJid participant, boolean kicked) {
			logger.debug("{} leaves group chat {}", participant.toString(), chatRoom.getChatJid().toString());
		}
	}
	
	public static final String WEBCHAT_VFS_FOLDER = "WebChat";
	private static final String UPLOAD_CONTEXT_WEBCHAT = "UploadWebChatFile";
	
	private class OnUploadCloudFile implements IServiceUploadStreamListener {
		@Override
		public void onUpload(String context, HttpServletRequest request, HashMap<String, String> multipartParams, WebTopSession.UploadedFile file, InputStream is, MapItem responseData) throws UploadException {
			
			if (context.equals(UPLOAD_CONTEXT_WEBCHAT)) {
				final IVfsManager vfsMgr = getVfsManager();
				if (xmppCli == null) throw new UploadException("XMPPClient not available");
				if (vfsMgr == null) throw new UploadException("VfsManager not available");
				
				try {
					String chatId = ServletUtils.getStringParameter(request, "chatId", true);
					
					final String path = "/" + WEBCHAT_VFS_FOLDER + "/" + chatId + "/";
					int myDocsStoreId = vfsMgr.getMyDocumentsStoreId();
					
					FileObject foPath = vfsMgr.getStoreFile(myDocsStoreId, path);
					foPath.createFolder(); // Ensure hierarchy existence
					String newPath = vfsMgr.addStoreFileFromStream(myDocsStoreId, path, file.getFilename(), is, false);
					FileObject foNew = vfsMgr.getStoreFile(myDocsStoreId, newPath);
					
					SharingLink sl = new SharingLink.BuilderForAdd(myDocsStoreId, newPath)
							.permanent()
							.free()
							.silent()
							.build();
					sl = vfsMgr.addDownloadLink(sl);
					URI[] urls = vfsMgr.getSharingLinkPublicURLs(sl);
					
					ChatMessage message = xmppCli.sendFileMessage(chatId, file.getFilename(), urls[2].toString(), file.getMediaType(), foNew.getContent().getSize());
					
					
					JsGridIMMessage js = new JsGridIMMessage(true, message, getEnv().getProfile().getTimeZone());
					responseData.put("id", js.id);
					responseData.put("fromId", js.fromId);
					responseData.put("fromNick", js.fromNick);
					responseData.put("isSent", js.isSent);
					responseData.put("timestamp", js.timestamp);
					responseData.put("action", js.action);
					responseData.put("text", js.text);
					responseData.put("data", js.data);
					responseData.put("fromArchive", js.fromArchive);
					
				} catch(UploadException ex) {
					throw ex;
				} catch(Throwable t) {
					logger.error("Error uploading chat file", t);
					throw new UploadException("Upload failure");
				}
			}
		}
	}
}
