/*
 * � Copyright IBM Corp. 2011, 2016
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at:
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
 * implied. See the License for the specific language governing 
 * permissions and limitations under the License.
 */

package com.ibm.domino.das.servlet;

import static com.ibm.domino.das.service.RestService.URL_PARAM_OWNER;
import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_STREAMING;
import static com.ibm.domino.services.rest.RestParameterConstants.PARAM_VALUE_TRUE;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_CODE;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_TEXT;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import lotus.domino.Session;

import org.apache.wink.server.utils.RegistrationUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;

import com.ibm.commons.log.Log;
import com.ibm.commons.log.LogMgr;
import com.ibm.commons.util.StringUtil;
import com.ibm.commons.util.io.json.JsonException;
import com.ibm.commons.util.io.json.JsonGenerator.Generator;
import com.ibm.commons.util.io.json.JsonGenerator.StringBuilderGenerator;
import com.ibm.commons.util.io.json.JsonJavaFactory;
import com.ibm.domino.commons.RequestContext;
import com.ibm.domino.commons.model.Customer;
import com.ibm.domino.commons.model.ICustomerProvider;
import com.ibm.domino.commons.model.IGatekeeperProvider;
import com.ibm.domino.commons.model.IStatisticsProvider;
import com.ibm.domino.commons.model.ProviderFactory;
import com.ibm.domino.das.service.CoreService;
import com.ibm.domino.das.service.DataService;
import com.ibm.domino.das.service.IRestServiceExt;
import com.ibm.domino.das.service.RestService;
import com.ibm.domino.das.servlet.DasStats.MutableDouble;
import com.ibm.domino.das.servlet.DasStats.MutableInteger;
import com.ibm.domino.das.utils.ErrorHelper;
import com.ibm.domino.das.utils.ScnContext;
import com.ibm.domino.das.utils.StatsContext;
import com.ibm.domino.osgi.core.context.ContextInfo;
import com.ibm.domino.services.AbstractRestServlet;
import com.ibm.domino.services.util.JsonWriter;
import com.ibm.xsp.acl.NoAccessSignal;


// Servlet created within the correct class loader
// This is a workaround to set the correct context class loader 
@SuppressWarnings("serial") // $NON-NLS-1$
public class DasServlet extends AbstractRestServlet {
    
    public static final LogMgr DAS_LOGGER = Log.load("com.ibm.domino.das");  //$NON-NLS-1$
    
    /**
     * The variable name [RestWebServices] that could be set in either Internet Site document for a domain,
     *  or a Server document for a specific server
     * This variable will return a list of services that are enabled or "" if no rest services are enabled.
     * 
     */
    private static final String CONFIG_RESTWEBSERVICES = "RestWebServices"; //$NON-NLS-1$
    
    private static final String DATA_SERVICE_NAME = "Data";//$NON-NLS-1$
    private static final String DATA_SERVICE_PATH = "data";//$NON-NLS-1$
    private static final String VERSION_ZERO = "0.0.0";  //$NON-NLS-1$
    private static final String DATA_SERVICE_VERSION = "9.0.1"; //$NON-NLS-1$
    
    private static final String CORE_SERVICE_NAME = "Core";//$NON-NLS-1$
    private static final String CORE_SERVICE_PATH = "core";//$NON-NLS-1$
    private static final String CORE_SERVICE_VERSION = "9.0.1"; //$NON-NLS-1$
    private static final int CORE_SERVICE_GKF = 427; // Defined by core SAAS code
    
    private static final long STATS_TIMER_INTERVAL = 30000;
    private static final String STATS_FACILITY = "API"; //$NON-NLS-1$
    private static final String STATS_REQUESTS = "Requests"; //$NON-NLS-1$
    private static final String STATS_TIME = "RequestTime"; //$NON-NLS-1$
    private static final String STATS_AVG_TIME = "AvgRequestTime"; //$NON-NLS-1$
    private static final String STATS_TOTAL_REQUESTS = "Total." + STATS_REQUESTS; //$NON-NLS-1$
    private static final String STATS_TOTAL_TIME = "Total." + STATS_TIME; //$NON-NLS-1$
    private static final String STATS_TOTAL_AVG_TIME = "Total." + STATS_AVG_TIME; //$NON-NLS-1$

    private static Boolean s_initialized = new Boolean(false);
    
    private static Timer s_statsTimer = null;
    private static TimerTask s_statsTimerTask = new TimerTask() {

        @Override
        public void run() {
            
            // Write all stats to Domino.  We do this on a timer
            // to avoid unneccessary churn.
            
            IStatisticsProvider provider = ProviderFactory.getStatisticsProvider();
            if ( provider != null ) {
                
                Set<Map.Entry<String, Object>> set = DasStats.get().getEntries();
                for ( Map.Entry<String, Object> entry : set ) {
                    Object value = entry.getValue();
                    if ( value instanceof MutableInteger ) {
                        int iValue = ((MutableInteger)value).getValue();
                        provider.UpdateInt(STATS_FACILITY, entry.getKey(), false, iValue);
                    }
                    else if ( value instanceof MutableDouble ) {
                        double dValue = ((MutableDouble)value).getValue();
                        provider.UpdateNumber(STATS_FACILITY, entry.getKey(), dValue);
                    }
                }
            }
        }
        
    };
    
    private static Map<String, DasService> s_services = new HashMap<String, DasService>();
    private static String s_enabledServices = null;

    private static Pattern s_acceptLanguagePattern = Pattern.compile("(\\w{1,8})(?:\\-(\\w{1,8}))?(?:\\;q=(\\d(?:\\.\\d)?))?"); // $NON-NLS-1$

    /**
     * Private class used to keep track of what services are enabled.
     */
    private class DasService {
        private String _name;
        private String _path;
        private boolean _enabled = false;
        private String _version;
        private int _gkf; // Gatekeeper feature #
        private boolean _allowSstUsers;
        private Application _application;
        private boolean _initialized= false;
        
        public DasService(String name, String path, String version, int gkf, 
                    boolean allowSstUsers, Application application) {
            _name = name;
            _path = path;
            _version = version;
            _gkf = gkf;
            _allowSstUsers = allowSstUsers;
            _application = application;
        }

        public boolean isEnabled() {
            return _enabled;
        }

        public void setEnabled(boolean enabled) {
            _enabled = enabled;
        }

        public String getName() {
            return _name;
        }

        public String getPath() {
            return _path;
        }
        
        public String getVersion() {
            return _version;
        }

        public Application getApplication() {
            return _application;
        }
        
        public void setInitialized(boolean initialized) {
            _initialized = initialized;
        }
        
        public boolean isInitialized() {
            return _initialized;
        }
        
        public int getGkf() {
            return _gkf;
        }

        public boolean isAllowSstUsers() {
            return _allowSstUsers;
        }
    }
    
    public void doInit() throws ServletException {
        synchronized(s_initialized) {
            if ( !s_initialized ) {
                super.doInit();
                
                // Initialize the core service
                initCoreService();
                
                // Initialize the data service
                initDataService();
                
                // Initialize resources from other plugins
                initDynamicResources();
                
                // Schedule the timer task to run every 30 seconds
                IStatisticsProvider provider = ProviderFactory.getStatisticsProvider();
                if ( provider != null ) {
                    s_statsTimer = new Timer(true);
                    s_statsTimer.schedule(s_statsTimerTask, STATS_TIMER_INTERVAL, STATS_TIMER_INTERVAL);
                }
                
                s_initialized = true;
                DAS_LOGGER.getLogger().fine("DasServlet initialized."); // $NON-NLS-1$
            }
        }
    }
    
    public void doDestroy() {
        if ( s_initialized ) {
            Iterator<String> iterator = s_services.keySet().iterator();
            while(iterator.hasNext()) {
                String key = iterator.next();
                DasService service = s_services.get(key);
                Application application = service.getApplication();
                if ( application instanceof RestService ) {
                    try {
                        ((RestService)application).destroy();
                    }
                    catch(Throwable e) {
                        DAS_LOGGER.warn(e, e.getMessage());
                    }
                }
            }
            
            // Clean up Domino stats
            
            IStatisticsProvider provider = ProviderFactory.getStatisticsProvider();
            if ( provider != null ) {
                
                Set<Map.Entry<String, Object>> set = DasStats.get().getEntries();
                for ( Map.Entry<String, Object> entry : set ) {
                    provider.Delete(STATS_FACILITY, entry.getKey());
                }
            }
        }
        super.doDestroy();
    }
    
    /**
     * Override the service to do the access control check before processing the REST request
     */
    @Override
    public void doService(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        StatsContext.init();
        
        try {
            // CSRF protection
            if (!verifyDasTokens(request)){
                handleError(response, Response.Status.FORBIDDEN, null);
                return;
            }
            
            // Set the SCN context
            setScnContext(request);
            
            // Get an instance of DasService for this request.  The
            // instance may be null.
            DasService service = getService(request);
            Application app = null;
            if ( service != null ) {
                app = service.getApplication();
            }
            
            // Make sure the service is enabled for this server or internet site
            if ( ! serviceEnabled(service) ) {
                handleError(response, Response.Status.FORBIDDEN, null);
                return;             
            }
            
            // Set the request context
            setRequestContext(request);
            
            //Wrap the http request for X-HTTP-Method-Override header manipulation
            DasHttpRequestWrapper requestWrapper = new DasHttpRequestWrapper(request);
            
            //Wrap the http response for Gzip/Deflate the output stream
            DasHttpResponseWrapper responseWrapper = new DasHttpResponseWrapper(request, response);
            
            // Enable streaming.
            String param = requestWrapper.getParameter(PARAM_STREAMING);  
            if (param != null) {
                boolean preventCache = param.contentEquals(PARAM_VALUE_TRUE); 
                responseWrapper.setPreventCache(preventCache);
            }
            
            try {
                if (app instanceof IRestServiceExt) {
                    if (((IRestServiceExt) app).beforeDoService(request)) {
                        super.doService(requestWrapper, responseWrapper);
                        ((IRestServiceExt) app).afterDoService(request);
                    }
                    else {
                        handleError(responseWrapper, Response.Status.FORBIDDEN, null);
                    }
                } 
                else {
                    super.doService(requestWrapper, responseWrapper);
                }
            }
            catch (ServletException e) {
                Throwable cause = e.getCause();
                if ( cause instanceof NoAccessSignal ) {
                    throw (NoAccessSignal)cause;
                }
                else {
                    handleUnknownException(app, request, responseWrapper, e);
                }
            }
            catch (Throwable e) {
                // Avoid throwing unknown exceptions to the container
                handleUnknownException(app, request, responseWrapper, e);
            }
        }
        finally {
            DasStats stats = DasStats.get();
            Date now = new Date();
            long elapsed = now.getTime() - StatsContext.getCurrentInstance().getStartTime().getTime();
            int requests = stats.addInteger(STATS_TOTAL_REQUESTS, 1);
            double requestTime = stats.addNumber(STATS_TOTAL_TIME, elapsed);

            // The average time is an approximation because we are not synchronizing threads.
            // When multiple threads update the same stat at the same time, the calculation could
            // be off, but it's not worth keeping a thread waiting for better precision.
            
            if ( requests != 0 ) {
                stats.setInteger(STATS_TOTAL_AVG_TIME, (int)(requestTime/requests));
            }
            
            String serviceName = StatsContext.getCurrentInstance().getServiceName();
            if ( StringUtil.isNotEmpty(serviceName) ) {
                requests = stats.addInteger(serviceName + "." + STATS_TOTAL_REQUESTS, 1);
                requestTime = stats.addNumber(serviceName + "." + STATS_TOTAL_TIME, elapsed);
                if ( requests != 0 ) {
                    stats.setInteger(serviceName + "." + STATS_TOTAL_AVG_TIME, (int)(requestTime/requests));
                }
                
                String category = StatsContext.getCurrentInstance().getRequestCategory();
                if ( StringUtil.isNotEmpty(category) ) {
                    requests = stats.addInteger(serviceName + "." + category + "." + STATS_REQUESTS, 1);
                    requestTime = stats.addNumber(serviceName + "." + category + "." + STATS_TIME, elapsed);
                    if ( requests != 0 ) {
                        stats.setInteger(serviceName + "." + category + "." + STATS_AVG_TIME, (int)(requestTime/requests));
                    }
                }
            }
        }
    }

    /**
     * @param request
     * @return
     */
    private boolean verifyDasTokens(HttpServletRequest request) {
        String CSRFCookieName = "DasToken"; // $NON-NLS-1$
        String CSRFHeaderName = "X-DAS-Token"; // $NON-NLS-1$

        boolean verified = true;
            
        Cookie[] cookies = request.getCookies();
        if (cookies == null)
                return verified;
        for (Cookie cookie : cookies)
        {
            String name = cookie.getName();
            if (name.equals(CSRFCookieName)){
                String csrfHeader = request.getHeader(CSRFHeaderName);
                if( csrfHeader == null || !cookie.getValue().equals(csrfHeader)){
                    verified = false;
                } 
                break;
            }      
        }
        
        return verified;
    }

    public static String getServicesResponse(String baseUrl) throws IOException, JsonException {
        refreshServiceMap();
        
        StringBuilder sb = new StringBuilder();
        Generator generator = new StringBuilderGenerator(JsonJavaFactory.instanceEx, sb, false);
        generator.out("{");
        generator.nl();
        generator.incIndent();

        generator.indent();
        generator.outPropertyName("services"); // $NON-NLS-1$
        generator.out(":[");
        generator.nl();
        generator.incIndent();
        
        Iterator<String> iterator = s_services.keySet().iterator();
        while(iterator.hasNext()) {
            String key = iterator.next();
            DasService service = s_services.get(key);
            
            generator.indent();
            generator.out("{");
            generator.nl();
            generator.incIndent();
            
            generator.indent();
            generator.outPropertyName("name"); // $NON-NLS-1$
            generator.out(":");
            generator.outLiteral(service.getName());
            generator.out(",");
            generator.nl();
            
            generator.indent();
            generator.outPropertyName("enabled"); // $NON-NLS-1$
            generator.out(":");
            generator.outBooleanLiteral(service.isEnabled());
            generator.out(",");
            generator.nl();
            
            generator.indent();
            generator.outPropertyName("version"); // $NON-NLS-1$
            generator.out(":");
            generator.outLiteral(service.getVersion());
            generator.out(",");
            generator.nl();
            
            generator.indent();
            generator.outPropertyName("href"); // $NON-NLS-1$
            generator.out(":");
            generator.outLiteral(baseUrl + service.getPath());
            generator.nl();
            generator.decIndent();

            generator.indent();
            generator.out("}");
            if ( iterator.hasNext() ) {
                generator.out(",");
            }
            generator.nl();
        }
        
        generator.decIndent();
        generator.indent();
        generator.out("]");
        
        generator.decIndent();
        generator.nl();
        generator.indent();
        generator.out("}");

        return sb.toString();
    }
    
    private void initCoreService() {
        
        try {
            Application application = new CoreService();
            
            // Add the service to our map
            DasService service = new DasService(CORE_SERVICE_NAME, CORE_SERVICE_PATH, 
                                        CORE_SERVICE_VERSION, CORE_SERVICE_GKF, 
                                        false, application);
            s_services.put(CORE_SERVICE_PATH, service);

            DAS_LOGGER.getLogger().fine("Registered the core DAS service"); // $NON-NLS-1$
        }
        catch (Throwable e) {
            DAS_LOGGER.warn(e, e.getMessage());
        }
    }
    
    private void initDataService() {
        
        try {
            Application application = new DataService();
            
            // Add the service to our map
            DasService service = new DasService(DATA_SERVICE_NAME, DATA_SERVICE_PATH, 
                                        DATA_SERVICE_VERSION, 0, false, application);
            s_services.put(DATA_SERVICE_PATH, service);

            DAS_LOGGER.getLogger().fine("Registered the data service"); // $NON-NLS-1$
        }
        catch (Throwable e) {
            DAS_LOGGER.warn(e, e.getMessage());
        }
    }
    
    /**
     * Initialize dynamic resources from other plugins.
     */
    private void initDynamicResources() {
        
        // Get a list of all registered extensions
        
        IExtension extensions[] = null;
        final IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
        if (extensionRegistry != null) {
            final IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint("com.ibm.domino.das.service"); // $NON-NLS-1$
            if (extensionPoint != null) {
                extensions = extensionPoint.getExtensions();
            }
        }
        
        if (extensions == null) {
            return;
        }

        // Walk through each extension in the list
        
        for (final IExtension extension : extensions) {
            final IConfigurationElement configElements[] = extension.getConfigurationElements();
            if (configElements == null) {
                continue;
            }
            
            for (final IConfigurationElement configElement : configElements) {
                try {
                    // We only handle serviceResources elements for now
                    DAS_LOGGER.getLogger().fine("Config element: " + configElement.getName()); // $NON-NLS-1$
                    if ( !("serviceResources".equalsIgnoreCase(configElement.getName())) ) { // $NON-NLS-1$
                        continue;
                    }
                    
                    String serviceName = configElement.getAttribute("name"); // $NON-NLS-1$
                    String servicePath = configElement.getAttribute("path"); // $NON-NLS-1$
                    String serviceVersion = configElement.getAttribute("version"); // $NON-NLS-1$
                    String serviceGkf = configElement.getAttribute("gkf"); // $NON-NLS-1$
                    if ( serviceName == null || servicePath == null ) {
                        DAS_LOGGER.getLogger().warning(StringUtil.format("DAS service {0} ignored. Both name and path must be defined.", serviceName)); // $NLW-DasServlet.DASservice0ignoredBothnameandpath-1$
                        continue;
                    }
                    
                    // Parse the gatekeeper feature #
                    int iGkf = 0;
                    if ( StringUtil.isNotEmpty(serviceGkf)) {
                        try {
                            iGkf = Integer.parseInt(serviceGkf);
                        }
                        catch (Throwable e) {
                            // Ignore parser exception
                        }
                    }

                    // Parse the SST option
                    String serviceAllowSstUsers = configElement.getAttribute("allowSstUsers"); // $NON-NLS-1$
                    boolean allowSstUsers = "true".equalsIgnoreCase(serviceAllowSstUsers); // $NON-NLS-1$
                    
                    DAS_LOGGER.getLogger().fine(StringUtil.format("Found a DAS service extension: {0} (/{1})", serviceName, servicePath)); // $NON-NLS-1$

                    final Object object = configElement.createExecutableExtension("class"); // $NON-NLS-1$
                    if ( ! (object instanceof Application) ) {
                        // Class was constructed but it is the wrong type
                        DAS_LOGGER.getLogger().warning(StringUtil.format("DAS service {0} ignored. Class is the wrong type.", serviceName)); // $NLW-DasServlet.DASservice0ignoredClassisthewrong-1$
                        continue;
                    }

                    // This is a critical section.  Things can go wrong inside registerApplication
                    // (e.g. NoClassDefFound).  So catch all exceptions and log them, but continue 
                    // to the next service.
                    
                    try {
                        Application application = (Application) object;
                        
                        // Add the service to our map
                        DasService service = new DasService(serviceName, servicePath, 
                                                    (serviceVersion != null) ? serviceVersion : VERSION_ZERO,
                                                     iGkf, allowSstUsers, application);
                        s_services.put(servicePath.toLowerCase(), service);
    
                        DAS_LOGGER.getLogger().fine("Registered a DAS service extension"); // $NON-NLS-1$
                    }
                    catch (Throwable e) {
                        DAS_LOGGER.warn(e, e.getMessage());
                    }
                } catch (final CoreException e) {
                    DAS_LOGGER.error(e, e.getMessage());
                }
            }
        }
    }
    
    /**
     * Tests whether the service corresponding to this request is enabled.
     * 
     * @param request
     * @return
     */
    private boolean serviceEnabled(DasService service) {
        if ( !isDominoServer() ) {
            // We only handle requests on the domino server
            return false;
        }

        if ( service == null ) {
            // We don't have an instance of DasService.  Let
            // Wink handle the request.
            return true;
        }
        
        // Initialize stats context for the request
        
        StatsContext.getCurrentInstance().setServiceName(service.getName());

        // Make sure service is enabled on this server
        
        boolean enabled = service.isEnabled();
        if ( !enabled ) {
            if ( DAS_LOGGER.getLogger().isLoggable(Level.FINE)) {
                DAS_LOGGER.getLogger().fine(StringUtil.format(
                        "The {0} service is disabled on this server.", // $NON-NLS-1$ 
                        service.getName()));                        
            }
        }

        // Do some SAAS-only checks
        
        if ( enabled && ScnContext.getCurrentInstance().isScn() ) {
            String customerId = ScnContext.getCurrentInstance().getCustomerId();

            // Service is enabled on this server.  Now check the 
            // gatekeeper feature.  When running in SAAS, this makes sure
            // the service is enabled for the customer.
            if ( service.getGkf() != 0 ) {
                String userId = ScnContext.getCurrentInstance().getUserId();
                IGatekeeperProvider provider = ProviderFactory.getGatekeeperProvider();
                enabled = provider.isFeatureEnabled(service.getGkf(), 
                            customerId, userId);

                if ( !enabled && DAS_LOGGER.getLogger().isLoggable(Level.FINE)) {
                    DAS_LOGGER.getLogger().fine(StringUtil.format(
                            "The {0} service is disabled by the gatekeeper for customer {1}.", // $NON-NLS-1$ 
                            service.getName(), 
                            customerId));                        
                }
            }

            // Even if the service is enabled thru gatekeeper, prevent
            // self service trial customers from making API requests.
            if ( enabled && StringUtil.isNotEmpty(customerId) && !service.isAllowSstUsers()) {
                try {
                    ICustomerProvider provider = ProviderFactory.getCustomerProvider();
                    if ( provider != null ) {
                        Customer customer = provider.getCustomer(customerId);
                        if ( customer.isSelfTrial() ) {
                            enabled = false;
                        }
                    }
                }
                catch (Throwable e) {
                    // Do nothing.  Assume the request is NOT for a self service trial customer.
                }
            }
        }

        // Just in time initialization

        if (enabled && !service.isInitialized()) {
            synchronized(service) {
                try {
                    RegistrationUtils.registerApplication(service.getApplication(), getServletContext());
                    service.setInitialized(true);
                }
                catch (Throwable e) {
                    enabled = false;
                    service.setEnabled(false);
                    DAS_LOGGER.warn(e, StringUtil.format(
                            "Automatically disabling {0} service because there was an error registering its resources.", // $NLW-DasServlet.Automaticallydisablingaservicebec-1$ 
                            service.getName()));
                }
            }
        }
        
        return enabled;
    }
    
    /**
     * Gets an instance of <code>DasService</code> from a request
     * 
     * @param request
     * @return
     */
    private DasService getService(HttpServletRequest request) {
        String requestPath = null;
        DasService service = null;
        
        if (request.getPathInfo() != null) {
            StringTokenizer tokenizer = new StringTokenizer(request.getPathInfo(), "/");
            try {
                requestPath = tokenizer.nextToken();
                if (requestPath != null) {
                    requestPath.toLowerCase();
                }
            } 
            catch (NoSuchElementException e) {
                // Ignore this
            }
        }

        if (requestPath != null) {
            refreshServiceMap();
            service = s_services.get(requestPath);
        }

        return service;
    }

    /**
     * Refreshes the service map.
     * 
     * <p>This method enables all the services listed in the Internet Site document
     * or server document.
     */
    private static void refreshServiceMap() {
        
        String enabledServices = ContextInfo.getServerVariable(CONFIG_RESTWEBSERVICES);
        if ( enabledServices.equals(s_enabledServices) ) {
            // No change in the list of services.  We're done.
            return;
        }
 
        synchronized(s_services) {
            
            // Disable all services except the core service
            
            Iterator<String> iterator = s_services.keySet().iterator();
            while (iterator.hasNext()) {
                DasService service = s_services.get(iterator.next());
                if ( CORE_SERVICE_NAME.equals(service.getName()) ) {
                    service.setEnabled(true);
                }
                else {
                    service.setEnabled(false);
                }
            }
            
            // Enable just the services in the list 
            
            StringTokenizer tokenizer = new StringTokenizer(enabledServices, ", ");
            while ( tokenizer.hasMoreTokens() ) {
                String serviceName = tokenizer.nextToken();
                
                // Enable the service
                iterator = s_services.keySet().iterator();
                while (iterator.hasNext()) {
                    DasService service = s_services.get(iterator.next());
                    if ( serviceName.equalsIgnoreCase(service.getName())) {
                        service.setEnabled(true);
                        break;
                    }
                }
            }
            
            // Remember the last value
            
            s_enabledServices = enabledServices;
        }
    }
    
    private boolean isDominoServer() {
        boolean isServer = false;
        Session session = null;
        
        try {
            session = ContextInfo.getUserSession();
            isServer = session.isOnServer();
        }
        catch (Throwable e) {
            // Ignore the exception.  Just assume we are not on the server.
        }
        
        return isServer;
    }
    
    /**
     * Handles an unknown exception.
     * 
     * <p>When a service throws <code>WebApplicationException</code>, the Wink
     * framework catches it and creates the correct error response.  This method
     * handles unexpected excecptions (e.g. NullPointerException).
     * 
     * @param app
     * @param request
     * @param response
     * @param t
     * @throws ServletException
     * @throws IOException
     */
    private void handleUnknownException(Application app, HttpServletRequest request, 
                    HttpServletResponse response, Throwable t) 
                    throws ServletException, IOException {
        
        // Share exception with the relevant service
        if (app instanceof IRestServiceExt) {
            try {
                ((IRestServiceExt) app).onUnknownError(request, t);
            }
            catch (Throwable e) {
                // Ignore exceptions
            }
        }

        // Create an error response
        handleError(response, Response.Status.INTERNAL_SERVER_ERROR, t);
    }
    
    /**
     * Writes the error to the HTTP response.
     * 
     * @param response
     * @param status
     * @param t
     * @throws ServletException
     * @throws IOException
     */
    private void handleError(HttpServletResponse response, Response.Status status, Throwable t) throws ServletException, IOException {
        
        String message = status.getReasonPhrase();
        
        try {
            response.sendError(status.getStatusCode(), message);
            response.setContentType(MediaType.APPLICATION_JSON);
            StringWriter writer = new StringWriter();
            Boolean compact = false;
            JsonWriter jWriter = new JsonWriter(writer, compact);
            
            try {
                jWriter.startObject();
                ErrorHelper.writeProperty(jWriter, ATTR_CODE, status.getStatusCode());
                ErrorHelper.writeProperty(jWriter, ATTR_TEXT, message);
                if ( t != null ) {
                    ErrorHelper.writeException(jWriter, t);
                }
            } 
            finally {
                jWriter.endObject();
            }

            StringBuffer buffer = writer.getBuffer();
            ServletOutputStream os = response.getOutputStream();
            os.print(buffer.toString());
            os.flush();
        }
        catch (IOException e) {
            DAS_LOGGER.warn(e, "Error creating response.");  //$NLW-DasServlet.Errorcreatingresponse-1$
        }
    }
    
    /**
     * Sets the request context for lower level utilities -- like 
     * string resouce handlers.
     * 
     * @param request
     */
    private void setRequestContext(HttpServletRequest request) {
        Locale userLocale = null;
        List<Locale> locales = parseLocales(request);
        if ( locales != null && locales.size() > 0 ) {
            userLocale = locales.get(0);
        }
        
        RequestContext ctx = RequestContext.getCurrentInstance();
        ctx.setUserLocale(userLocale);
        ctx.setCustomerId(ScnContext.getCurrentInstance().getCustomerId());
    }
    
    /**
     * Sets the SCN context.
     * 
     * @param request
     */
    private void setScnContext(HttpServletRequest request) {
        ScnContext ctx = ScnContext.init();

        String dominoDn = request.getHeader("X-DominoDN"); // $NON-NLS-1$
        String dominoSamlTo = request.getHeader("X-DominoSAMLTo"); // $NON-NLS-1$
        if ( StringUtil.isNotEmpty(dominoDn) && StringUtil.isNotEmpty(dominoSamlTo) ) {
            ctx.setScn(true);
        }
        
        String ownerId = request.getParameter(URL_PARAM_OWNER);
        ctx.setOwnerId(ownerId);

        String customerId = request.getHeader("X-DominoCustomerID"); // $NON-NLS-1$
        if ( StringUtil.isNotEmpty(customerId) ) {
            ctx.setCustomerId(customerId);
        }
        
        String userId = request.getHeader("X-DominoUserID"); // $NON-NLS-1$
        if ( StringUtil.isNotEmpty(userId) ) {
            ctx.setUserId(userId);
        }

    }
    
    /**
     * Parses all Accept-Language headers.
     * 
     * @param request
     * @return
     */
    private List<Locale> parseLocales(HttpServletRequest request)
    {
        List<Locale> locales = null;
        
        try {
        
            Enumeration values = request.getHeaders("accept-language"); // $NON-NLS-1$
            while(values.hasMoreElements())
            {
                if ( locales == null ) {
                    locales = new ArrayList<Locale>();
                }
                String value = values.nextElement().toString();
                parseLocales(locales, value);
            }
        }
        catch (Throwable t) {
            // Shouldn't happen, but just in case, ignore parser errors
            // and other unchecked exceptions
        }
        
        return locales;
    }

    /**
     * Parse a single Accept-Language header.
     * 
     * @param locales
     * @param headerValue
     */
    private void parseLocales(List<Locale> locales, String headerValue)
    {
        // Portions of this method were stolen from XspCmdHttpServletRequest.  The
        // following comment is from there:
        //
        // Http Header parsing
        // Accept-Language = "Accept-Language" ":"
        // 1#( language-range [ ";" "q" "=" qvalue ] )
        // qvalue = ( "0" [ "." 0*3DIGIT ] )
        // | ( "1" [ "." 0*3("0") ] )
        // language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
        // Each language-range MAY be given an associated quality value which represents an estimate of the user's
        // preference for the languages specified by that range. The quality value defaults to "q=1". For example,
        //
        // Accept-Language: da, en-gb;q=0.8, en;q=0.7

        String[] s = StringUtil.splitString(headerValue, ',', true);

        for(int i = 0; i < s.length; i++)
        {
            Matcher matcher = s_acceptLanguagePattern.matcher(s[i]);
            if(matcher.find())
            {
                String l1 = matcher.group(1);
                if(l1 == null)
                {
                    l1 = "";
                }
                String l2 = matcher.group(2);
                if(l2 == null)
                {
                    l2 = "";
                }
                Locale l = new Locale(l1, l2);

                // TODO: Handle quality. Although we are parsing it, we
                // are throwing it away for now.
                
                double q = 1.0;
                String qs = matcher.group(3);
                if(qs != null)
                {
                    q = Double.parseDouble(qs);
                }

                locales.add(l);
            }
        }
    }
}