com.sonicle.webtop.core.app.WebTopSession.java Source code

Java tutorial

Introduction

Here is the source code for com.sonicle.webtop.core.app.WebTopSession.java

Source

/*
 * 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.webtop.core.app.sdk.BaseDocEditorDocumentHandler;
import com.sonicle.commons.time.DateTimeUtils;
import com.sonicle.webtop.core.sdk.UserProfile;
import com.sonicle.security.Principal;
import com.sonicle.webtop.core.CoreLocaleKey;
import com.sonicle.webtop.core.CoreManager;
import com.sonicle.webtop.core.CoreServiceSettings;
import com.sonicle.webtop.core.CoreUserSettings;
import com.sonicle.webtop.core.admin.CoreAdminManager;
import com.sonicle.webtop.core.app.servlet.UIPrivate;
import com.sonicle.webtop.core.msg.AutosaveMessage;
import com.sonicle.webtop.core.bol.OAutosave;
import com.sonicle.webtop.core.bol.js.JsWTS;
import com.sonicle.webtop.core.bol.js.JsWTSPrivate;
import com.sonicle.webtop.core.bol.js.JsWTSPublic;
import com.sonicle.webtop.core.model.ServicePermission;
import com.sonicle.webtop.core.model.ServiceSharePermission;
import com.sonicle.webtop.core.sdk.BaseManager;
import com.sonicle.webtop.core.sdk.BasePublicService;
import com.sonicle.webtop.core.sdk.BaseService;
import com.sonicle.webtop.core.sdk.ServiceManifest;
import com.sonicle.webtop.core.sdk.ServiceMessage;
import com.sonicle.webtop.core.sdk.UserProfileId;
import com.sonicle.webtop.core.sdk.WTException;
import com.sonicle.webtop.core.sdk.WTRuntimeException;
import com.sonicle.webtop.core.app.servlet.Otp;
import com.sonicle.webtop.core.util.IdentifierUtils;
import com.sonicle.webtop.core.util.LoggerUtils;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.mail.Authenticator;
import javax.mail.PasswordAuthentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import net.sf.uadetector.ReadableDeviceCategory;
import net.sf.uadetector.ReadableUserAgent;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.joda.time.DateTime;
import org.slf4j.Logger;

/**
 *
 * @author malbinola
 */
public class WebTopSession {
    private static final Logger logger = WT.getLogger(WebTopSession.class);
    public static final String PROP_REQUEST_DUMP = "REQUESTDUMP";

    private final WebTopApp wta;
    private HttpSession session;
    private final String csrfToken;
    private Boolean jsDebugEnabled;

    private final Object lock0 = new Object();
    private ReadableUserAgent readableUserAgent = null;
    private final PropertyBag propsBag = new PropertyBag();
    private int initLevel = 0;
    private UserProfile profile = null;
    private PrivateEnvironment privateEnv = null;
    private CorePrivateEnvironment privateCoreEnv = null;
    private PublicEnvironment publicEnv = null;
    private final HashMap<String, BaseManager> managers = new HashMap<>();
    private Set<String> allowedServices = null;
    private final LinkedHashMap<String, BaseService> privateServices = new LinkedHashMap<>();
    private final LinkedHashMap<String, BasePublicService> publicServices = new LinkedHashMap<>();
    private final HashMap<String, UploadedFile> uploads = new HashMap<>();
    private final Object lock1 = new Object();
    private javax.mail.Session mailSession = null;

    public WebTopSession(HttpSession session) {
        this(WebTopApp.getInstance(), session);
    }

    WebTopSession(WebTopApp wta, HttpSession session) {
        this.wta = wta;
        this.session = session;
        this.csrfToken = IdentifierUtils.getCRSFToken();
        this.jsDebugEnabled = WebTopProps.getJsDebug();
    }

    synchronized void cleanup() throws Exception {
        initLevel = -1;

        emptyPrivateServices();
        emptyPublicServices();
        emptyServiceManagers();

        // Cleanup uploads
        String domainId = getProfileDomainId();
        if (domainId != null) {
            synchronized (uploads) {
                for (UploadedFile upf : uploads.values()) {
                    if (!upf.isVirtual())
                        wta.deleteTempFile(domainId, upf.getUploadId());
                }
                uploads.clear();
            }
        }

        DocEditorManager docEdMgr = wta.getDocEditorManager();
        if (docEdMgr != null)
            docEdMgr.cleanupOnSessionDestroy(session.getId());
    }

    // TODO: rimuovere metodi deprecati

    /**
     * @deprecated use {@link #getClientRemoteIP()} instead.
     * @return 
     */
    @Deprecated
    public String getRemoteIP() {
        return getClientRemoteIP();
    }

    /**
     * @deprecated use {@link #getClientPlainUserAgent()} instead.
     * @return 
     */
    @Deprecated
    public String getPlainUserAgent() {
        return getClientPlainUserAgent();
    }

    /**
     * Returns the associated user session.
     * @return Session object
     */
    public HttpSession getSession() {
        return session;
    }

    /**
     * Convenience method to get genenated CSRF token.
     * @return Generated CSRF token
     */
    public String getCSRFToken() {
        return csrfToken;
    }

    /**
     * Convenience method to get WebTop genenated client identifier.
     * @return WebTop client identifier
     */
    public String getClientTrackingID() {
        return SessionContext.getWebTopClientID(session);
    }

    /**
     * Convenience method to get client's IP address.
     * @return The network address
     */
    public String getClientRemoteIP() {
        return SessionContext.getClientRemoteIP(session);
    }

    /**
     * Convenience method to get client's browser URL.
     * @return The network address
     */
    public String getClientUrl() {
        return SessionContext.getClientUrl(session);
    }

    /**
     * Convenience method to get client's plain user-agent info.
     * @return user-agent info
     */
    public String getClientPlainUserAgent() {
        return SessionContext.getClientPlainUserAgent(session);
    }

    /**
     * Convenience method to get client's parsed user-agent info.
     * @return A readable ReadableUserAgent object. 
     */
    public ReadableUserAgent getClientUserAgent() {
        synchronized (lock0) {
            if (readableUserAgent == null) {
                String plainUa = getClientPlainUserAgent();
                if (!StringUtils.isBlank(plainUa)) {
                    readableUserAgent = WebTopApp.getUserAgentInfo(plainUa);
                }
            }
            return readableUserAgent;
        }
    }

    /**
     * Convenience method to get the referer-uri.
     * @return The referer-uri
     */
    public String getRefererUri() {
        return SessionContext.getRefererUri(session);
    }

    /**
     * Returns the session ID.
     * @return Session unique identifier
     */
    public String getId() {
        return session.getId().toString();
    }

    /**
     * Returns current configuration for js debug.
     * @return 
     */
    public boolean isJsDebugEnabled() {
        return jsDebugEnabled;
    }

    /**
     * Return current locale.
     * It can be the UserProfile's locale or the locale specified during
     * the initial HTTP request to the server.
     * @return The locale.
     */
    public Locale getLocale() {
        if (profile != null) {
            return profile.getLocale();
        } else {
            return SessionContext.getClientLocale(session);
        }
    }

    /**
     * Associates an object to this session, using the key specified.
     * @param serviceId The service ID to which the object is bound.
     * @param key The key to which the object is mapped.
     * @param value The object to be mapped.
     * @return The object just associated.
     */
    public Object setProperty(String serviceId, String key, Object value) {
        synchronized (propsBag) {
            propsBag.set(serviceId + "@" + key, value);
            return value;
        }
    }

    /**
     * Returns the object to which the specified key is mapped, or null if no object is mapped under the key.
     * @param serviceId The service ID to which the object is mapped.
     * @param key The key to which the object is mapped.
     * @return If found, the object to which the specified key is mapped.
     */
    public Object getProperty(String serviceId, String key) {
        synchronized (propsBag) {
            return propsBag.get(serviceId + "@" + key);
        }
    }

    /**
     * Returns the object to which the specified key is mapped, or null if no object is mapped under the key.
     * Mapping will be cleared when the object is returned.
     * @param serviceId The service ID to which the object is mapped.
     * @param key The key to which the object is mapped.
     * @return If found, the object to which the specified key is mapped.
     */
    public Object popProperty(String serviceId, String key) {
        synchronized (propsBag) {
            if (hasProperty(serviceId, key)) {
                Object value = propsBag.get(serviceId + "@" + key);
                clearProperty(serviceId, key);
                return value;
            } else {
                return null;
            }
        }
    }

    /**
     * Clears the object mapped to the specified key.
     * @param serviceId The service ID to which the object is mapped.
     * @param key The key to which the object is mapped.
     */
    public void clearProperty(String serviceId, String key) {
        synchronized (propsBag) {
            propsBag.clear(serviceId + "@" + key);
        }
    }

    /**
     * Checks if this session contains a mapping for the specified key.
     * @param serviceId The service ID to which the object is mapped.
     * @param key The key to which the object is mapped.
     * @return True if a mapping is found, false otherwise.
     */
    public boolean hasProperty(String serviceId, String key) {
        synchronized (propsBag) {
            return propsBag.has(serviceId + "@" + key);
        }
    }

    /**
     * Checks if this session contains a mapping for the specified key,
     * otherwise throws an exception.
     * @param serviceId The service ID to which the object is mapped.
     * @param key The key to which the object is mapped.
     * @throws WTException 
     */
    public void hasPropertyOrThrow(String serviceId, String key) throws WTException {
        if (!hasProperty(serviceId, key))
            throw new WTException("Missing session property [{0}, {1}]", serviceId, key);
    }

    public boolean isReady() {
        return initLevel == 2;
    }

    /**
     * Gets the user profile associated to the session.
     * @return The UserProfile.
     */
    public UserProfile getUserProfile() {
        return profile;
    }

    /**
     * Gets the UserProfile's ID.
     * Note that this can be null if the user is not authenticated. (eg. public area)
     * @return The UserProfile's ID
     */
    public UserProfileId getProfileId() {
        return (profile == null) ? null : profile.getId();
    }

    /**
     * Gets Profile's Domain ID.
     * Note that if the user is not authenticated the virtual domain ID is returned instead.
     * @return The profile's domain ID
     */
    public String getProfileDomainId() {
        if (profile == null) {
            //TODO: restituire l'id del dominio decodificato dall'host
            return null;
        } else {
            return profile.getDomainId();
        }
    }

    /**
     * Gets Profile's User ID.
     * Note that this can be null if the user is not authenticated. (eg. public area)
     * @return The profile's user ID
     */
    public String getProfileUserId() {
        return (profile == null) ? null : profile.getUserId();
    }

    public synchronized void initPrivate(HttpServletRequest request) throws WTException {
        if (initLevel < 0)
            return;
        if (initLevel == 0)
            internalInitPrivate(request);
    }

    public synchronized void initPrivateEnvironment(HttpServletRequest request) throws WTException {
        if (initLevel < 0)
            return;
        if (initLevel == 0)
            throw new WTException("You need to call initPrivate() before calling this method!");
        if (initLevel == 1)
            internalInitPrivateEnvironment(request);
    }

    private void internalInitPrivate(HttpServletRequest request) throws WTException {
        // Synchronization on caller method!
        ServiceManager svcm = wta.getServiceManager();
        Principal principal = (Principal) SecurityUtils.getSubject().getPrincipal();

        Subject subject = RunContext.getSubject();
        UserProfileId profileId = RunContext.getRunProfileId(subject);
        session.setAttribute(SessionManager.ATTRIBUTE_GUESSING_USERNAME, profileId.toString());

        emptyServiceManagers();

        CoreManager core = svcm.instantiateCoreManager(false, profileId);
        cacheServiceManager(CoreManifest.ID, core);
        CoreAdminManager coreadmin = svcm.instantiateCoreAdminManager(false, profileId);
        cacheServiceManager(CoreAdminManifest.ID, coreadmin);

        // Defines useful instances (NB: keep code assignment order!!!)
        profile = new UserProfile(core, principal);

        boolean passwordChangeNeeded = wta.getWebTopManager().isUserPasswordChangeNeeded(profileId,
                principal.getPassword());
        if (passwordChangeNeeded && !principal.isImpersonated())
            setProperty(CoreManifest.ID, UIPrivate.WTSPROP_PASSWORD_CHANGEUPONLOGIN, true);

        boolean otpEnabled = wta.getOTPManager().isEnabled(profile.getId());
        if (!otpEnabled || principal.isImpersonated())
            setProperty(CoreManifest.ID, Otp.WTSPROP_OTP_VERIFIED, true);

        initLevel = 1;
    }

    public void internalCleanupPrivateEnvironment() {
        // Synchronization on caller method!

        allowedServices = null;

        privateCoreEnv = null;
        privateEnv = null;
    }

    private Set<String> listAllowedPrivateServices(ServiceManager svcm) {
        LinkedHashSet<String> ids = new LinkedHashSet<>();
        if (RunContext.isSysAdmin()) {
            ids.add(CoreManifest.ID);
            ids.add(CoreAdminManifest.ID);
            ids.add("com.sonicle.webtop.vfs");

        } else {
            for (String id : svcm.listRegisteredServices()) {
                if (RunContext.isPermitted(true, CoreManifest.ID, "SERVICE", "ACCESS", id))
                    ids.add(id);
            }
        }
        return ids;
    }

    private void internalInitPrivateEnvironment(HttpServletRequest request) throws WTException {
        // Synchronization on caller method!
        ServiceManager svcm = wta.getServiceManager();
        SessionManager sesm = wta.getSessionManager();
        CoreManager core = WT.getCoreManager(profile.getId());

        privateCoreEnv = new CorePrivateEnvironment(wta, this);
        privateEnv = new PrivateEnvironment(this);

        wta.getLogManager().write(profile.getId(), CoreManifest.ID, "AUTHENTICATED", null, request, getId(), null);
        sesm.registerWebTopSession(this);
        allowedServices = listAllowedPrivateServices(svcm);

        BaseManager managerInst = null;
        for (String serviceId : allowedServices) {
            ServiceDescriptor descriptor = svcm.getDescriptor(serviceId);

            // Manager
            // Skip core service... its manager has already been instantiated above (see: internalInitPrivate)
            if (!serviceId.equals(CoreManifest.ID) && !serviceId.equals(CoreAdminManifest.ID)) {
                if (descriptor.hasManager() && !isServiceManagerCached(serviceId)) {
                    managerInst = svcm.instantiateServiceManager(serviceId, false, profile.getId());
                    if (managerInst != null) {
                        cacheServiceManager(serviceId, managerInst);
                    }
                }
            }
        }

        BaseService privateInst = null;
        for (String serviceId : allowedServices) {
            ServiceDescriptor descriptor = svcm.getDescriptor(serviceId);

            // Service initialization
            svcm.prepareProfile(serviceId, profile.getId());

            // PrivateService
            if (descriptor.hasPrivateService()) {
                // Creates new instance
                if (svcm.hasFullRights(serviceId)) {
                    privateInst = svcm.instantiatePrivateService(serviceId, privateCoreEnv);
                } else {
                    privateInst = svcm.instantiatePrivateService(serviceId, privateEnv);
                }
                if (privateInst != null) {
                    cachePrivateService(privateInst);
                }
            }
        }

        logger.debug("Instantiated {} managers", managers.size());
        logger.debug("Instantiated {} private services", privateServices.size());

        /*
        // Instantiates services
        BaseService instance = null;
        List<String> serviceIds = core.listPrivateServices();
        int count = 0;
        // TODO: ordinamento lista servizi (scelta dall'utente?)
        for(String serviceId : serviceIds) {
           // Creates new instance
           if(svcm.hasFullRights(serviceId)) {
        instance = svcm.instantiatePrivateService(serviceId, sessionId, new CoreEnvironment(wta, this));
           } else {
        instance = svcm.instantiatePrivateService(serviceId, sessionId, new Environment(this));
           }
           if(instance != null) {
        registerPrivateService(instance);
        count++;
           }
        }
            
        logger.debug("Instantiated {} services", count);
        */

        initLevel = 2;

        String cid = getClientTrackingID();
        boolean mine = core.hasMyAutosaveData(cid);
        List<OAutosave> odata = core.listOfflineOthersAutosaveData(cid);
        boolean others = (odata == null) ? false : odata.size() > 0;
        if (mine || others) {
            this.notify(new AutosaveMessage(core.SERVICE_ID, mine, others));
        }

    }

    public synchronized void initPublicEnvironment(HttpServletRequest request, String publicServiceId)
            throws WTException {
        internalInitPublicEnvironment(request, publicServiceId);
    }

    public void internalCleanupPublicEnvironment() {
        // Synchronization on caller method!
        emptyPublicServices();
        publicEnv = null;
    }

    private void internalInitPublicEnvironment(HttpServletRequest request, String publicServiceId)
            throws WTException {
        // Synchronization on caller method!

        if (isPublicServiceCached(publicServiceId))
            return;
        ServiceManager svcm = wta.getServiceManager();

        if (!isServiceManagerCached(CoreManifest.ID)) {
            CoreManager core = svcm.instantiateCoreManager(true, RunContext.getRunProfileId());
            cacheServiceManager(CoreManifest.ID, core);
        }
        if (publicEnv == null)
            publicEnv = new PublicEnvironment(this);

        int managersCount = 0, publicCount = 0;
        String[] serviceIds = new String[] { CoreManifest.ID, publicServiceId };

        BaseManager managerInst = null;
        for (String serviceId : serviceIds) {
            ServiceDescriptor descriptor = svcm.getDescriptor(serviceId);

            // Manager (skip core)
            if (!serviceId.equals(CoreManifest.ID)) {
                if (descriptor.hasManager() && !isServiceManagerCached(serviceId)) {
                    managerInst = svcm.instantiateServiceManager(serviceId, true, RunContext.getRunProfileId());
                    if (managerInst != null) {
                        cacheServiceManager(serviceId, managerInst);
                        managersCount++;
                    }
                }
            }
        }

        BasePublicService publicInst = null;
        for (String serviceId : serviceIds) {
            ServiceDescriptor descriptor = svcm.getDescriptor(serviceId);

            // PublicService
            if (descriptor.hasPublicService() && !isPublicServiceCached(serviceId)) {
                publicInst = svcm.instantiatePublicService(serviceId, publicEnv);
                if (publicInst != null) {
                    cachePublicService(publicInst);
                    publicCount++;
                }
            }
        }

        logger.debug("Instantiated {} managers", managersCount);
        logger.debug("Instantiated {} public services", publicCount);
    }

    public boolean isServiceAllowed(String serviceId) {
        return allowedServices.contains(serviceId);
    }

    private void cacheServiceManager(String serviceId, BaseManager manager) {
        synchronized (managers) {
            if (managers.containsKey(serviceId))
                throw new WTRuntimeException("Cannot add manager twice");
            managers.put(serviceId, manager);
        }
    }

    public boolean isServiceManagerCached(String serviceId) {
        synchronized (managers) {
            return managers.containsKey(serviceId);
        }
    }

    public BaseManager getServiceManager(String serviceId) {
        synchronized (managers) {
            if (!managers.containsKey(serviceId))
                return null;
            return managers.get(serviceId);
        }
    }

    private void emptyServiceManagers() {
        synchronized (managers) {
            managers.clear();
        }
    }

    public javax.mail.Session getMailSession() {
        synchronized (lock1) {
            UserProfileId pid = getProfileId();
            if (pid != null && mailSession == null) {
                CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, pid.getDomainId());
                String smtphost = css.getSMTPHost();
                int smtpport = css.getSMTPPort();
                boolean starttls = css.isSMTPStartTLS();
                boolean auth = css.isSMTPAuthentication();
                Properties props = new Properties(System.getProperties());
                //props.setProperty("mail.socket.debug", "true");
                //props.setProperty("mail.imap.parse.debug", "true");
                props.setProperty("mail.smtp.host", smtphost);
                props.setProperty("mail.smtp.port", "" + smtpport);
                if (starttls) {
                    props.put("mail.smtp.starttls.enable", "true");
                    props.put("mail.smtp.ssl.trust", "*");
                    props.put("mail.smtp.ssl.checkserveridentity", "false");
                }
                props.setProperty("mail.imaps.ssl.trust", "*");
                props.setProperty("mail.imap.folder.class", "com.sonicle.mail.imap.SonicleIMAPFolder");
                props.setProperty("mail.imaps.folder.class", "com.sonicle.mail.imap.SonicleIMAPFolder");
                props.setProperty("mail.imap.enableimapevents", "true"); // Support idle events
                Authenticator authenticator = null;
                if (auth) {
                    props.setProperty("mail.smtp.auth", "true");
                    authenticator = new Authenticator() {
                        @Override
                        protected PasswordAuthentication getPasswordAuthentication() {
                            Principal principal = (Principal) SecurityUtils.getSubject().getPrincipal();
                            String login = principal.getFullInternetName();
                            String password = new String(principal.getPassword());
                            //logger.info("getPasswordAuthentication: "+login+" / *****");
                            return new PasswordAuthentication(login, password);
                        }

                    };
                }
                mailSession = javax.mail.Session.getInstance(props, authenticator);
            }
        }
        return mailSession;
    }

    /**
     * Stores private service instance into this session.
     * @param service 
     */
    private void cachePrivateService(BaseService service) {
        String serviceId = service.getManifest().getId();
        synchronized (privateServices) {
            if (privateServices.containsKey(serviceId))
                throw new WTRuntimeException("Cannot add private service twice");
            privateServices.put(serviceId, service);
        }
    }

    /**
     * Gets a private service instance by ID.
     * @param serviceId The service ID.
     * @return The service instance, if found.
     */
    public BaseService getPrivateServiceById(String serviceId) {
        if (!isReady())
            return null;
        synchronized (privateServices) {
            if (!privateServices.containsKey(serviceId))
                throw new WTRuntimeException("No private service with ID [{0}]", serviceId);
            return privateServices.get(serviceId);
        }
    }

    public List<String> getPrivateServices() {
        return getPrivateServices(false);
    }

    /**
     * Gets instantiated services list.
     * @param sortByOrder True to sort the list using chosen order
     * @return A list of service ids.
     */
    public List<String> getPrivateServices(boolean sortByOrder) {
        if (!isReady())
            return null;
        synchronized (privateServices) {
            List<String> ids = Arrays.asList(privateServices.keySet().toArray(new String[privateServices.size()]));
            if (sortByOrder) {
                CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, getUserProfile().getDomainId());
                sortServiceIdsByOrder(css.getServicesOrder(), ids);
            }
            return ids;
        }
    }

    private void emptyPrivateServices() {
        ServiceManager svcm = wta.getServiceManager();
        synchronized (privateServices) {
            for (BaseService instance : privateServices.values()) {
                svcm.cleanupPrivateService(instance);
            }
            privateServices.clear();
        }
    }

    /**
     * Stores public service instance into this session.
     * @param service 
     */
    private void cachePublicService(BasePublicService service) {
        String serviceId = service.getManifest().getId();
        synchronized (publicServices) {
            if (publicServices.containsKey(serviceId))
                throw new WTRuntimeException("Cannot add public service twice");
            publicServices.put(serviceId, service);
        }
    }

    /**
     * Checks if a public service instance exists.
     * @param serviceId The service ID.
     * @return True if instance is present, false otherwise.
     */
    private boolean isPublicServiceCached(String serviceId) {
        return publicServices.containsKey(serviceId);
    }

    /**
     * Gets a public service instance by ID.
     * @param serviceId The service ID.
     * @return The service instance, if found.
     */
    public BasePublicService getPublicServiceById(String serviceId) {
        synchronized (publicServices) {
            if (!publicServices.containsKey(serviceId))
                throw new WTRuntimeException("No public service with ID [{0}]", serviceId);
            return publicServices.get(serviceId);
        }
    }

    /**
     * Gets instantiated public services list.
     * @return A list of service ids.
     */
    public List<String> getPublicServices() {
        synchronized (publicServices) {
            return Arrays.asList(publicServices.keySet().toArray(new String[publicServices.size()]));
        }
    }

    private void emptyPublicServices() {
        ServiceManager svcm = wta.getServiceManager();
        synchronized (publicServices) {
            for (BasePublicService instance : publicServices.values()) {
                svcm.cleanupPublicService(instance);
            }
            publicServices.clear();
        }
    }

    public void fillStartup(JsWTSPrivate js) {
        if (!isReady())
            return;

        ServiceManager svcm = wta.getServiceManager();
        ServiceManifest coreManifest = svcm.getManifest(CoreManifest.ID);
        CoreUserSettings cus = new CoreUserSettings(CoreManifest.ID, profile.getId());
        String theme = cus.getTheme(), layout = cus.getLayout(), lookAndFeel = cus.getLookAndFeel();
        ReadableDeviceCategory.Category deviceCategory = getClientUserAgent().getDeviceCategory().getCategory();
        if (ReadableDeviceCategory.Category.SMARTPHONE.equals(deviceCategory)
                || ReadableDeviceCategory.Category.TABLET.equals(deviceCategory)) {
            if (theme.equals("crisp") || theme.equals("neptune")) {
                theme += "-touch";
            } else {
                theme = "crisp-touch";
            }
        }
        Locale locale = getLocale();

        fillAppReferences(js, locale, theme, false);
        js.layoutClassName = StringUtils.capitalize(layout);

        List<String> privateSids = getPrivateServices(true);

        // Include Core references
        //js.appManifest.name = coreManifest.getJsPackageName();
        fillServiceManifest(js, coreManifest, locale, svcm.isInMaintenance(coreManifest.getId()));
        fillCoreServiceJsReferences(svcm.isInDevMode(CoreManifest.ID), "private", js, coreManifest, locale);
        fillServiceCssReferences(js, coreManifest, theme, lookAndFeel);

        // Include other services references
        if (RunContext.isWebTopAdmin()) {
            for (String sid : svcm.listRegisteredServices()) {
                if (sid.equals(CoreManifest.ID))
                    continue;
                ServiceDescriptor descriptor = svcm.getDescriptor(sid);
                fillServiceReferences(svcm, js, descriptor, locale, theme, lookAndFeel);
            }
        } else {
            for (String sid : privateSids) {
                if (sid.equals(CoreManifest.ID))
                    continue;
                ServiceDescriptor descriptor = svcm.getDescriptor(sid);
                fillServiceReferences(svcm, js, descriptor, locale, theme, lookAndFeel);
            }
        }

        fillRolesMap(js.roles);

        // Evaluate services
        for (String sid : privateSids) {
            ServiceDescriptor descriptor = svcm.getDescriptor(sid);
            //fillServiceReferences(svcm, js, descriptor, locale, theme, lookAndFeel);
            fillStartupForService(svcm, js, descriptor, locale);
        }
    }

    private void fillRolesMap(HashSet<String> roles) {
        Subject subject = RunContext.getSubject();
        pushIfSubjectHasRole(roles, subject, WebTopManager.ROLEUID_SYSADMIN);
        pushIfSubjectHasRole(roles, subject, WebTopManager.ROLEUID_WTADMIN);
        pushIfSubjectHasRole(roles, subject, WebTopManager.ROLEUID_IMPERSONATED_USER);
    }

    private void fillServiceReferences(ServiceManager svcm, JsWTS js, ServiceDescriptor descriptor, Locale locale,
            String theme, String lookAndFeel) {
        ServiceManifest manifest = descriptor.getManifest();
        if (manifest.getId().equals(CoreManifest.ID))
            throw new WTRuntimeException("Core service's references should not be added in this way");

        fillServiceManifest(js, manifest, locale, svcm.isInMaintenance(manifest.getId()));
        // Includes service references
        fillServiceJsReferences(svcm.isInDevMode(manifest.getId()), js, manifest, locale);
        // Includes service stylesheet references
        fillServiceCssReferences(js, manifest, theme, lookAndFeel);
    }

    private void fillStartupForService(ServiceManager svcm, JsWTSPrivate js, ServiceDescriptor descriptor,
            Locale locale) {
        ServiceManifest manifest = descriptor.getManifest();
        Subject subject = RunContext.getSubject();

        // Generates service auth permissions
        JsWTSPrivate.Permissions perms = new JsWTSPrivate.Permissions();
        for (ServicePermission perm : manifest.getDeclaredPermissions()) {
            if (perm instanceof ServiceSharePermission)
                continue;

            JsWTSPrivate.Actions acts = new JsWTSPrivate.Actions();
            for (String act : perm.getActions()) {
                if (RunContext.isPermitted(true, subject, manifest.getId(), perm.getGroupName(), act)) {
                    acts.put(act, true);
                }
            }
            if (!acts.isEmpty())
                perms.put(perm.getGroupName(), acts);
        }

        JsWTSPrivate.PrivateService jssvc2 = (JsWTSPrivate.PrivateService) js.createServiceInstance();
        jssvc2.id = manifest.getId();
        jssvc2.serviceCN = manifest.getPrivateServiceJsClassName(true);
        jssvc2.serviceVarsCN = manifest.getPrivateServiceVarsModelJsClassName(true);
        if (descriptor.hasUserOptionsService()) {
            jssvc2.userOptions = new JsWTSPrivate.ServiceUserOptions(manifest.getUserOptionsViewJsClassName(true),
                    manifest.getUserOptionsModelJsClassName(true));
        }
        for (ServiceManifest.Portlet portlet : manifest.getPortlets()) {
            jssvc2.portletCNs.add(portlet.jsClassName);
        }
        js.services.add(jssvc2);
        js.servicesVars.add(getServiceVars(manifest.getId()));
        js.servicesPerms.add(perms);
    }

    private JsWTSPrivate.Vars getServiceVars(String serviceId) {
        BaseService svc = getPrivateServiceById(serviceId);
        BaseService.ServiceVars vars = null;

        // Retrieves initial vars from instantiated service
        try {
            LoggerUtils.setContextDC(serviceId);
            vars = svc.returnServiceVars();
        } catch (Exception ex) {
            logger.error("returnServiceVars method returns errors", ex);
        } finally {
            LoggerUtils.clearContextServiceDC();
        }

        JsWTSPrivate.Vars is = new JsWTSPrivate.Vars();
        if (vars != null)
            is.putAll(vars);

        // Built-in settings
        if (serviceId.equals(CoreManifest.ID)) {
            //is.put("authTicket", generateAuthTicket());
            is.put("isWhatsnewNeeded", isWhatsnewNeeded());
        } else {
            CoreUserSettings cus = new CoreUserSettings(serviceId, profile.getId());
            is.put("viewportToolWidth", cus.getViewportToolWidth());
        }
        return is;
    }

    public void fillStartup(JsWTSPublic js, String publicServiceId) {
        Locale locale = getLocale();
        ServiceManager svcm = wta.getServiceManager();
        ServiceManifest coreManifest = svcm.getManifest(CoreManifest.ID);

        String theme = "crisp";
        ReadableDeviceCategory.Category deviceCategory = getClientUserAgent().getDeviceCategory().getCategory();
        if (ReadableDeviceCategory.Category.SMARTPHONE.equals(deviceCategory)
                || ReadableDeviceCategory.Category.TABLET.equals(deviceCategory)) {
            theme += "-touch";
        }

        fillAppReferences(js, locale, theme, false);

        // Include Core references
        //js.appManifest.name = coreManifest.getJsPackageName();
        fillServiceManifest(js, coreManifest, locale, svcm.isInMaintenance(coreManifest.getId()));
        fillCoreServiceJsReferences(true/*svcm.isInDevMode(CoreManifest.ID)*/, "public", js, coreManifest, locale);
        fillServiceCssReferences(js, coreManifest, theme, "default");

        fillStartupForPublicService(js, CoreManifest.ID, locale);
        fillStartupForPublicService(js, publicServiceId, locale);
    }

    private void fillStartupForPublicService(JsWTSPublic js, String serviceId, Locale locale) {
        ServiceManager svcm = wta.getServiceManager();
        ServiceDescriptor sdesc = svcm.getDescriptor(serviceId);
        ServiceManifest manifest = sdesc.getManifest();

        // Fill application manifest with service references (NOTE: core service is skipped here!)
        if (!serviceId.equals(CoreManifest.ID)) {
            fillServiceManifest(js, manifest, locale, svcm.isInMaintenance(serviceId));
            // Includes service references
            fillServiceJsReferences(true/*svcm.isInDevMode(serviceId)*/, js, manifest, locale);
            // Includes service stylesheet references
            fillServiceCssReferences(js, manifest, "crisp", "default");
        }

        JsWTSPublic.PublicService jssvc2 = (JsWTSPublic.PublicService) js.createServiceInstance();
        jssvc2.id = manifest.getId();
        jssvc2.serviceCN = manifest.getPublicServiceJsClassName(true);
        jssvc2.serviceVarsCN = manifest.getPublicServiceVarsModelJsClassName(true);
        js.services.add(jssvc2);
        js.servicesVars.add(getPublicServiceVars(serviceId));
    }

    private JsWTSPublic.Vars getPublicServiceVars(String serviceId) {
        BasePublicService svc = getPublicServiceById(serviceId);
        BasePublicService.ServiceVars vars = null;

        // Retrieves initial vars from instantiated service
        if (svc != null) {
            try {
                LoggerUtils.setContextDC(serviceId);
                vars = svc.returnServiceVars();
            } catch (Exception ex) {
                logger.error("returnServiceVars method returns errors", ex);
            } finally {
                LoggerUtils.clearContextServiceDC();
            }
        }

        JsWTSPublic.Vars is = new JsWTSPublic.Vars();
        if (vars != null)
            is.putAll(vars);
        return is;
    }

    private void fillAppReferences(JsWTS js, Locale locale, String theme, boolean rtl) {
        js.appManifest.name = "Sonicle.webtop.core.app.App";
        js.appManifest.id = "5ae25afe-182c-466c-a6ad-0a3af0ee74b5";
        js.appManifest.theme = theme;
        js.themeName = theme;
        js.platformName = wta.getPlatformName();
        js.fileTypes = wta.getFileTypes().toString();
        fillExtJsReferences(js, locale, theme, rtl);
    }

    private void fillExtJsReferences(JsWTS js, Locale locale, String theme, boolean rtl) {
        js.appManifest.framework = "ext";
        js.appManifest.toolkit = "classic";

        //TODO: rendere dinamico il caricamento delle librerie, permettendo ai servizi di aggiungere le loro

        // Do not replace 0.0.0 with the real version, it limits server traffic.
        final String VENDOR_PATH = "resources/com.sonicle.webtop.core/0.0.0/resources/vendor";
        final String LIBS_PATH = "resources/com.sonicle.webtop.core/0.0.0/resources/libs";

        // Include external libraries references
        js.appManifest.addJs(VENDOR_PATH + "/jquery/3.3.1/" + "jquery.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/spark-md5/3.0.0/" + "spark-md5.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/js-emoji/3.4.1/" + "emoji.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/ion.sound/3.0.7/" + "ion.sound.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/linkify/2.1.6/" + "linkify.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/linkify/2.1.6/" + "linkify-string.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/screenfull/3.3.2/" + "screenfull.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/atmosphere/2.3.5/" + "atmosphere.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/jsxc/3.4.0/" + "jsxc.dep.js");
        js.appManifest.addJs(VENDOR_PATH + "/tinymce/4.3.12/" + "tinymce.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/plupload/2.3.6/" + "plupload.full.min.js"); // Remember to update paths in Factory.js
        js.appManifest.addJs(VENDOR_PATH + "/rrule/2.1.0/" + "rrule.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/markjs/8.11.1/" + "mark.min.js");
        js.appManifest.addJs(VENDOR_PATH + "/search-string/3.1.0/" + "search-string.min.js");

        // Uncomment these lines to load debug versions of the libraries ----->
        //js.appManifest.addJs(VENDOR_PATH + "/jsxc/3.4.0/" + "jsxc.dep.js");
        //js.appManifest.addJs(VENDOR_PATH + "/tinymce/4.3.12/" + "tinymce.js");
        //js.appManifest.addJs(VENDOR_PATH + "/plupload/2.3.6/" + "moxie.js");
        //js.appManifest.addJs(VENDOR_PATH + "/plupload/2.3.6/" + "plupload.dev.js");
        // <-------------------------------------------------------------------
        //js.appManifest.addJs(VENDOR_PATH + "/ckeditor/" + "ckeditor.js");

        // Include ExtJs references
        final String EXTJS_PATH = "resources/client/extjs/";
        String extRtl = rtl ? "-rtl" : "";
        String extDebug = WebTopProps.getExtJsDebug() ? "-debug" : "";
        String extTheme = theme;
        String extBaseTheme = StringUtils.removeEnd(theme, "-touch");
        String extLang = "-" + locale.getLanguage();
        js.appManifest.addJs(EXTJS_PATH + "ext-all" + extRtl + extDebug + ".js");
        js.appManifest
                .addJs(EXTJS_PATH + js.appManifest.toolkit + "/locale/" + "locale" + extLang + extDebug + ".js");
        //js.appManifest.addJs(EXTJS_PATH + "packages/ext-locale/build/" + "ext-locale" + extLang + extDebug + ".js"); // ExtJs library localization
        js.appManifest.addJs(EXTJS_PATH + js.appManifest.toolkit + "/" + "theme-" + extTheme + "/" + "theme-"
                + extTheme + extDebug + ".js");
        js.appManifest.addCss(EXTJS_PATH + js.appManifest.toolkit + "/" + "theme-" + extTheme + "/resources/"
                + "theme-" + extTheme + "-all" + extRtl + extDebug + ".css");
        //js.appManifest.addJs(EXTJS_PATH + "packages/" + "ext-theme-" + extTheme + "/build/" + "ext-theme-" + extTheme + extDebug + ".js"); // ExtJs theme overrides
        //js.appManifest.addCss(EXTJS_PATH + "packages/" + "ext-theme-" + extTheme + "/build/resources/" + "ext-theme-" + extTheme + "-all" + extRtl + extDebug + ".css");
        js.appManifest.addJs(
                EXTJS_PATH + "packages/charts/" + js.appManifest.toolkit + "/" + "charts" + extDebug + ".js");
        js.appManifest.addCss(EXTJS_PATH + "packages/charts/" + js.appManifest.toolkit + "/" + extBaseTheme
                + "/resources/" + "charts-all" + extRtl + extDebug + ".css");
        //js.appManifest.addCss(EXTJS_PATH + "packages/sencha-charts/build/" + extBaseTheme + "/resources/" + "sencha-charts-all" + extRtl + extDebug + ".css");   
        js.appManifest.addJs(EXTJS_PATH + "packages/ux/" + js.appManifest.toolkit + "/" + "ux" + extDebug + ".js");
        js.appManifest.addCss(EXTJS_PATH + "packages/ux/" + js.appManifest.toolkit + "/" + extBaseTheme
                + "/resources/" + "ux-all" + extRtl + extDebug + ".css");

        // Include Sonicle ExtJs Extensions references
        if (WebTopProps.getSoExtJsExtensionsDevMode()) {
            js.appManifest.addPath("Sonicle", EXTJS_PATH + "packages/sonicle-extensions/src");
        } else {
            js.appManifest
                    .addJs(EXTJS_PATH + "packages/sonicle-extensions/" + "sonicle-extensions" + extDebug + ".js");
        }
        js.appManifest.addCss(EXTJS_PATH + "packages/sonicle-extensions/" + extBaseTheme + "/resources/"
                + "sonicle-extensions-all" + extRtl + extDebug + ".css");

        // Override default Ext error handling in order to avoid application hang.
        // NB: This is only necessary when using ExtJs debug file!
        if (WebTopProps.getExtJsDebug())
            js.appManifest.addJs(LIBS_PATH + "/" + "ext-override-errors.js");
    }

    private void fillCoreServiceJsReferences(boolean devMode, String target, JsWTS js, ServiceManifest manifest,
            Locale locale) {
        String targetSuffix = "-" + target;
        if (devMode) {
            String jsFileName = (js instanceof JsWTSPublic) ? manifest.getPrivateServiceJsFileName()
                    : manifest.getPrivateServiceJsFileName();
            js.appManifest.addJs(manifest.getPackageSrcUrl() + "/app/WT.js");
            js.appManifest.addJs(manifest.getPackageSrcUrl() + "/app/Factory.js");
            js.appManifest.addJs(manifest.getPackageSrcUrl() + "/app/Util.js");
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/src/app" + targetSuffix + ".js"); // App file (private or public)
            js.appManifest.addJs(manifest.getPackageSrcUrl() + "/" + jsFileName); // Service js class
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getLocaleJsFileName(locale)); // Service's locale js class
            js.appManifest.paths.put(manifest.getJsPackageName(), manifest.getPackageSrcUrl()); // Namespace -> url path mapping
            js.appManifest.paths.put("WTA", manifest.getPackageSrcUrl()); // Short namespace (WTA) -> url path mapping

        } else {
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getId() + targetSuffix + ".js"); // Service concatenated js
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getLocaleJsFileName(locale)); // Service's locale js class
        }
        js.locales.add(new JsWTS.XLocale(manifest.getId(), manifest.getLocaleJsClassName(locale, true)));
    }

    private void fillServiceManifest(JsWTS js, ServiceManifest manifest, Locale locale, boolean maintenance) {
        JsWTS.Manifest jsman = js.createManifestInstance();

        jsman.xid = manifest.getXId();
        jsman.ns = manifest.getJsPackageName();
        jsman.path = manifest.getJsBaseUrl(false);
        jsman.name = StringEscapeUtils
                .escapeJson(wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_NAME));
        jsman.description = StringEscapeUtils
                .escapeJson(wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_DESCRIPTION));
        jsman.company = StringEscapeUtils.escapeJson(manifest.getCompany());
        jsman.localeCN = manifest.getLocaleJsClassName(locale, true);
        jsman.maintenance = maintenance;

        if (jsman instanceof JsWTSPrivate.PrivateManifest) {
            ((JsWTSPrivate.PrivateManifest) jsman).version = manifest.getVersion().toString();
            ((JsWTSPrivate.PrivateManifest) jsman).build = manifest.getBuildDate();
        }

        js.manifests.put(manifest.getId(), jsman);

        //JsWTS.Manifest jsman = new JsWTS.Manifest();
        //jsman.xid = manifest.getXId();
        //jsman.ns = manifest.getJsPackageName();
        //jsman.path = manifest.getJsBaseUrl(false);
        //jsman.name = StringEscapeUtils.escapeJson(wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_NAME));
        //jsman.description = StringEscapeUtils.escapeJson(wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_DESCRIPTION));
        //jsman.version = manifest.getVersion().toString();
        //jsman.build = manifest.getBuildDate();
        //jsman.company = StringEscapeUtils.escapeJson(manifest.getCompany());
        //jsman.localeClassName = manifest.getLocaleJsClassName(locale, true);
        //js.manifests.put(manifest.getId(), jsman);

        //String localizedName = wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_NAME);
        //String localizedDescription = wta.lookupResource(manifest.getId(), locale, CoreLocaleKey.SERVICE_DESCRIPTION);
        //js.manifests.put(manifest.getId(), new JsWTS.Manifest(manifest, localizedName, localizedDescription, manifest.getLocaleJsClassName(locale, true)));
    }

    private void fillServiceJsReferences(boolean devMode, JsWTS js, ServiceManifest manifest, Locale locale) {
        if (devMode) {
            String jsFileName = (js instanceof JsWTSPublic) ? manifest.getPrivateServiceJsFileName()
                    : manifest.getPrivateServiceJsFileName();
            js.appManifest.paths.put(manifest.getJsPackageName(), manifest.getPackageSrcUrl()); // Namespace -> url path mapping
            js.appManifest.addJs(manifest.getPackageSrcUrl() + "/" + jsFileName); // Service js class
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getLocaleJsFileName(locale)); // Service's locale js class
        } else {
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getBundleJsFileName()); // Service concatenated js
            js.appManifest.addJs(manifest.getPackageBaseUrl() + "/" + manifest.getLocaleJsFileName(locale)); // Service's locale js class
        }
        js.locales.add(new JsWTS.XLocale(manifest.getId(), manifest.getLocaleJsClassName(locale, true)));
    }

    private void fillServiceCssReferences(JsWTS js, ServiceManifest manifest, String theme, String lookAndFeel) {
        js.appManifest.addCss(manifest.getPackageLookAndFeelUrl(lookAndFeel) + "/" + "service.css");
        js.appManifest.addCss(manifest.getPackageLookAndFeelUrl(lookAndFeel) + "/" + "service-override.css");
        js.appManifest.addCss(manifest.getPackageLookAndFeelUrl(lookAndFeel) + "/" + "service-" + theme + ".css");
        js.appManifest.addCss(
                manifest.getPackageLookAndFeelUrl(lookAndFeel) + "/" + "service-override-" + theme + ".css");
    }

    private List<String> sortServiceIdsByOrder(final CoreServiceSettings.ServicesOrder so, List<String> ids) {
        Collections.sort(ids, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                int i1 = serviceIdToOrderIndex(so, o1);
                int i2 = serviceIdToOrderIndex(so, o2);
                if (i1 < i2) {
                    return -1;
                } else if (i1 > i2) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        return ids;
    }

    private int serviceIdToOrderIndex(CoreServiceSettings.ServicesOrder so, String serviceId) {
        if (StringUtils.equals(serviceId, CoreManifest.ID)) {
            return -2;
        } else if (StringUtils.equals(serviceId, CoreAdminManifest.ID)) {
            return -1;
        } else {
            int i = so.indexOf(serviceId);
            return (i != -1) ? i : 99;
        }
    }

    private void pushIfSubjectHasRole(HashSet<String> roles, Subject subject, String hasRole) {
        if (RunContext.hasRole(subject, hasRole))
            roles.add(hasRole);
    }

    private boolean isWhatsnewNeeded() {
        ServiceManager svcm = wta.getServiceManager();
        boolean needWhatsnew = false;
        for (String serviceId : getPrivateServices()) {
            needWhatsnew = needWhatsnew | svcm.needWhatsnew(serviceId, profile);
        }
        return needWhatsnew;
    }

    public boolean needWhatsnew(String serviceId, UserProfile profile) {
        if (!isReady())
            return false;
        ServiceManager svcm = wta.getServiceManager();
        return svcm.needWhatsnew(serviceId, profile);
    }

    public String getWhatsnewHtml(String serviceId, UserProfile profile, boolean full) {
        if (!isReady())
            return null;
        ServiceManager svcm = wta.getServiceManager();
        return svcm.getWhatsnew(serviceId, profile, full);
    }

    public void resetWhatsnew(String serviceId, UserProfile profile) {
        if (!isReady())
            return;
        ServiceManager svcm = wta.getServiceManager();
        svcm.resetWhatsnew(serviceId, profile.getId());
    }

    public void notify(ServiceMessage message) {
        if (!isReady())
            return;
        wta.getSessionManager().push(getId(), message);
    }

    public void notify(List<ServiceMessage> messages) {
        if (!isReady())
            return;
        wta.getSessionManager().push(getId(), messages);
    }

    public void addUploadedFile(UploadedFile uploadedFile) {
        if (!isReady())
            return;
        synchronized (uploads) {
            uploads.put(uploadedFile.getUploadId(), uploadedFile);
        }
    }

    public UploadedFile getUploadedFile(String uploadId) {
        if (!isReady())
            return null;
        synchronized (uploads) {
            return uploads.get(uploadId);
        }
    }

    /**
     * Checks if there is an uploaded file entry with specified ID.
     * @param uploadId Uploaded file ID
     * @return True if present, false otherwise.
     */
    public boolean hasUploadedFile(String uploadId) {
        if (!isReady())
            return false;
        synchronized (uploads) {
            return uploads.containsKey(uploadId);
        }
    }

    /**
     * Removes the uploaded file entry from the storage.
     * @param uploadedFile The entry to remove
     * @param deleteTempFile True to remove also corresponding fisical file from Temp
     */
    public void removeUploadedFile(UploadedFile uploadedFile, boolean deleteTempFile) {
        removeUploadedFile(uploadedFile.getUploadId(), deleteTempFile);
    }

    /**
     * Removes the uploaded file entry from the storage.
     * @param uploadId Uploaded file ID
     * @param deleteTempFile True to remove also corresponding physical file from Temp
     */
    public void removeUploadedFile(String uploadId, boolean deleteTempFile) {
        if (!isReady())
            return;
        synchronized (uploads) {
            UploadedFile upf = uploads.get(uploadId);
            if (upf != null) {
                if (deleteTempFile && !upf.isVirtual()) {
                    String domainId = getProfileDomainId();
                    try {
                        wta.deleteTempFile(domainId, uploadId);
                    } catch (WTException ex) {
                        /* Do nothing... */ }
                }
                uploads.remove(uploadId);
            }
        }
    }

    /**
     * Remove uploaded files by tag value.
     * Files will be also deleted from Temp directory.
     * @param tag 
     */
    public void removeUploadedFileByTag(String tag) {
        if (!isReady())
            return;
        synchronized (uploads) {
            Iterator<Map.Entry<String, UploadedFile>> it = uploads.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, UploadedFile> entry = it.next();
                if (StringUtils.equals(entry.getValue().getTag(), tag)) {
                    if (!entry.getValue().isVirtual()) {
                        String domainId = getProfileDomainId();
                        try {
                            wta.deleteTempFile(domainId, entry.getValue().getUploadId());
                        } catch (WTException ex) {
                            /* Do nothing... */ }
                    }
                    it.remove();
                }
            }
        }
    }

    public DocEditorManager.DocumentConfig prepareDocumentEditing(BaseDocEditorDocumentHandler docHandler,
            String filename, long lastModifiedTime) throws WTException {
        return wta.getDocEditorManager().registerDocumentHandler(getId(), docHandler, filename, lastModifiedTime);
    }

    public void finalizeDocumentEditing(String editingId) {
        wta.getDocEditorManager().unregisterDocumentHandler(editingId);
    }

    public static class UploadedFile {
        private final boolean virtual;
        private final String serviceId;
        private final String uploadId;
        private final String tag;
        private final String filename;
        private final long size;
        private final String mediaType;
        private final DateTime uploadedOn;
        private HashMap<String, Object> properties = null;

        public UploadedFile(boolean virtual, String serviceId, String uploadId, String tag, String filename,
                long size, String mediaType) {
            this.virtual = virtual;
            this.serviceId = serviceId;
            this.uploadId = uploadId;
            this.tag = tag;
            this.filename = filename;
            this.size = size;
            this.mediaType = mediaType;
            this.uploadedOn = DateTimeUtils.now(true);
        }

        public boolean isVirtual() {
            return virtual;
        }

        public String getServiceId() {
            return serviceId;
        }

        public String getUploadId() {
            return uploadId;
        }

        public String getTag() {
            return tag;
        }

        public String getFilename() {
            return filename;
        }

        public Long getSize() {
            return size;
        }

        public String getMediaType() {
            return mediaType;
        }

        public DateTime getUploadedOn() {
            return uploadedOn;
        }

        public File getFile() throws WTException {
            return new File(WT.getTempFolder(), getUploadId());
        }

        public void setProperty(String key, Object value) {
            if (properties == null)
                properties = new HashMap();
            properties.put(key, value);
        }

        public Object getProperty(String key) {
            if (properties == null)
                return null;
            return properties.get(key);
        }
    }
}