001    /*
002     * Copyright © 2008, 2009 Pedro Agulló Soliveres.
003     * 
004     * This file is part of DirectJNgine.
005     *
006     * DirectJNgine is free software: you can redistribute it and/or modify
007     * it under the terms of the GNU Lesser General Public License as published by
008     * the Free Software Foundation, either version 3 of the License.
009     *
010     * Commercial use is permitted to the extent that the code/component(s)
011     * do NOT become part of another Open Source or Commercially developed
012     * licensed development library or toolkit without explicit permission.
013     *
014     * DirectJNgine is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017     * GNU Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public License
020     * along with DirectJNgine.  If not, see <http://www.gnu.org/licenses/>.
021     * 
022     * This software uses the ExtJs library (http://extjs.com), which is 
023     * distributed under the GPL v3 license (see http://extjs.com/license).
024     */
025    
026    package com.softwarementors.extjs.djn.servlet;
027    
028    import java.io.IOException;
029    import java.util.ArrayList;
030    import java.util.HashMap;
031    import java.util.List;
032    import java.util.Map;
033    
034    import javax.servlet.ServletConfig;
035    import javax.servlet.ServletException;
036    import javax.servlet.http.HttpServlet;
037    import javax.servlet.http.HttpServletRequest;
038    import javax.servlet.http.HttpServletResponse;
039    
040    import org.apache.commons.fileupload.FileItem;
041    import org.apache.commons.fileupload.FileUploadException;
042    import org.apache.commons.fileupload.servlet.ServletFileUpload;
043    import org.apache.log4j.Logger;
044    import org.apache.log4j.NDC;
045    
046    import com.softwarementors.extjs.djn.EncodingUtils;
047    import com.softwarementors.extjs.djn.StringUtils;
048    import com.softwarementors.extjs.djn.Timer;
049    import com.softwarementors.extjs.djn.api.Registry;
050    import com.softwarementors.extjs.djn.config.ApiConfiguration;
051    import com.softwarementors.extjs.djn.config.GlobalConfiguration;
052    import com.softwarementors.extjs.djn.gson.GsonBuilderConfigurator;
053    import com.softwarementors.extjs.djn.jscodegen.CodeFileGenerator;
054    import com.softwarementors.extjs.djn.router.RequestRouter;
055    import com.softwarementors.extjs.djn.router.RequestType;
056    import com.softwarementors.extjs.djn.router.dispatcher.Dispatcher;
057    import com.softwarementors.extjs.djn.router.dispatcher.DispatcherConfigurationException;
058    import com.softwarementors.extjs.djn.router.processor.RequestException;
059    import com.softwarementors.extjs.djn.router.processor.poll.PollRequestProcessor;
060    import com.softwarementors.extjs.djn.router.processor.standard.form.upload.UploadFormPostRequestProcessor;
061    import com.softwarementors.extjs.djn.router.processor.standard.json.JsonRequestProcessorThread;
062    import com.softwarementors.extjs.djn.scanner.Scanner;
063    import com.softwarementors.extjs.djn.servlet.config.RegistryConfigurationException;
064    import com.softwarementors.extjs.djn.servlet.ssm.SsmDispatcher;
065    import com.softwarementors.extjs.djn.servlet.ssm.SsmJsonRequestProcessorThread;
066    import com.softwarementors.extjs.djn.servlet.ssm.WebContextManager;
067    
068    import edu.umd.cs.findbugs.annotations.CheckForNull;
069    import edu.umd.cs.findbugs.annotations.NonNull;
070    
071    public class DirectJNgineServlet extends HttpServlet {
072    
073      private static final long serialVersionUID = -5621879599626932408L;
074    
075      @NonNull
076      private static final Logger logger = Logger.getLogger( DirectJNgineServlet.class);
077    
078      /*********************************************************  
079       * GlobalParameters and configuration
080       *********************************************************/
081      private static final String VALUES_SEPARATOR = ",";
082      public static final String REGISTRY_CONFIGURATOR_CLASS = "registryConfiguratorClass";
083      
084      /* We handle processors and uploaders via a static map for several reasons:
085    
086         1. Public fields are not ok: ServletFileUpload is simply not serializable, and
087            servlets are serializable. Besides, using servlet fields to hold state can
088            be problematic.
089         2. A single static variable is not ok, because it is possible to instantiate
090            a servlet several times via <servlet> entries in web.xml, each one having
091            a different configuration and therefore requiring a different RequestRouter and
092            ServletFileUpload.
093    
094         The solution was to have static maps, keyed by servlet name, which is always
095         unique in a web application.
096      */
097      @NonNull private static Map<String,RequestRouter> processors = new HashMap<String,RequestRouter>();
098      @NonNull private static Map<String,ServletFileUpload> uploaders = new HashMap<String,ServletFileUpload>();
099      
100      // Non-mutable => no need to worry about thread-safety => can be an 'instance' variable
101      protected RequestRouter getProcessor() {
102        assert processors.containsKey(getServletName());
103        
104        return processors.get(getServletName());
105      }
106      
107      // Non-mutable => no need to worry about thread-safety => can be an 'instance' variable
108      protected ServletFileUpload getUploader() {
109        assert uploaders.containsKey(getServletName());
110        
111        return uploaders.get(getServletName());
112      }
113      
114      // This can be static: we do not worry if we get the same id 
115      // in a load-balanced system
116      private static long id = 1000; // It is good for formatting to get lots of ids with the same number of digits...
117    
118      public static class GlobalParameters {
119        @NonNull public static final String PROVIDERS_URL = "providersUrl";
120        @NonNull public static final String DEBUG = "debug";
121        
122        @NonNull private static final String APIS_PARAMETER = "apis";
123        @NonNull private static final String MINIFY = "minify";
124    
125        @NonNull public static final String BATCH_REQUESTS_MULTITHREADING_ENABLED = "batchRequestsMultithreadingEnabled";
126        @NonNull public static final String BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE = "batchRequestsMinThreadsPoolSize";
127        @NonNull public static final String BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE = "batchRequestsMaxThreadsPoolSize";
128        @NonNull public static final String BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS = "batchRequestsMaxThreadKeepAliveSeconds";
129        @NonNull public static final String BATCH_REQUESTS_MAX_THREADS_PER_REQUEST = "batchRequestsMaxThreadsPerRequest";
130        @NonNull public static final String GSON_BUILDER_CONFIGURATOR_CLASS = "gsonBuilderConfiguratorClass";
131        @NonNull public static final String DISPATCHER_CLASS = "dispatcherClass";
132        @NonNull public static final String JSON_REQUEST_PROCESSOR_THREAD_CLASS = "jsonRequestProcessorThreadClass";
133        @NonNull public static final String CONTEXT_PATH = "contextPath";
134        @NonNull public static final String CREATE_SOURCE_FILES="createSourceFiles";
135      }
136    
137      public static class ApiParameters {
138        @NonNull public static final String API_FILE = "apiFile";
139        @NonNull public static final String API_NAMESPACE = "apiNamespace";
140        @NonNull public static final String ACTIONS_NAMESPACE = "actionsNamespace";
141        @NonNull public static final String CLASSES = "classes";
142      }
143    
144      private synchronized long getUniqueRequestId() {
145        return id++;
146      }
147      
148      @Override
149      public void init(ServletConfig configuration) throws ServletException
150      {
151        assert configuration != null;
152        super.init(configuration);
153        
154        Timer timer = new Timer();
155        createDirectJNgineRouter(configuration);
156        timer.stop();
157        timer.logDebugTimeInMilliseconds("Djn initialization: total DirectJNgine initialization time");
158      }
159    
160      protected void createDirectJNgineRouter(ServletConfig configuration) throws ServletException {
161        assert configuration != null;
162    
163        Timer subtaskTimer = new Timer();
164        GlobalConfiguration globalConfiguration = createGlobalConfiguration(configuration);
165        String registryConfiguratorClassName = ServletUtils.getParameter(configuration, REGISTRY_CONFIGURATOR_CLASS, null);   
166        if( logger.isInfoEnabled() ) {
167          String value = registryConfiguratorClassName;
168          if( value == null) {
169            value = "";
170          }
171          logger.info( "Servlet GLOBAL configuration: " + REGISTRY_CONFIGURATOR_CLASS + "=" + value ); 
172        }
173        
174        Class<? extends ServletRegistryConfigurator> registryConfiguratorClass = getRegistryConfiguratorClass(registryConfiguratorClassName);
175        List<ApiConfiguration> apiConfigurations = createApiConfigurationsFromServletConfigurationApi(configuration);
176        subtaskTimer.stop();
177        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Servlet Configuration Load time");
178        
179        subtaskTimer.restart();
180        Registry registry = new Registry( globalConfiguration );
181        Scanner scanner = new Scanner(registry);
182        scanner.scanAndRegisterApiConfigurations( apiConfigurations );
183        subtaskTimer.stop();
184        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Standard Api processing time");
185    
186        if( registryConfiguratorClass != null ) {
187          subtaskTimer.restart();
188          performCustomRegistryConfiguration( registryConfiguratorClass, registry, configuration );
189          subtaskTimer.stop();
190          subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Custom Registry processing time");
191        }
192        
193        subtaskTimer.restart();
194        try {
195          CodeFileGenerator.updateSource(registry, globalConfiguration.getCreateSourceFiles());      
196          subtaskTimer.stop();
197          subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Api Files creation time");
198        }
199        catch( IOException ex ) {
200          ServletException e = new ServletException( "Unable to create DirectJNgine API files",  ex );
201          logger.fatal( e.getMessage(), e );
202           throw e;
203        }
204    
205        subtaskTimer.restart();
206        
207        initializeRouter(globalConfiguration, registry);
208        
209        subtaskTimer.stop();
210        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Request Processor initialization time");    
211      }
212    
213      private void initializeRouter(GlobalConfiguration globalConfiguration, Registry registry) {
214        String servletName = getServletName();
215        uploaders.put( servletName, UploadFormPostRequestProcessor.createFileUploader() );
216        processors.put( servletName, createRequestRouter(registry, globalConfiguration) );
217      }
218    
219      protected RequestRouter createRequestRouter(Registry registry, GlobalConfiguration globalConfiguration) {
220        assert registry != null;
221        assert globalConfiguration != null;
222        
223        return new RequestRouter( registry, globalConfiguration, createDispatcher(globalConfiguration.getDispatcherClass()) );
224      }
225      
226      protected Dispatcher createDispatcher( Class<? extends Dispatcher> cls ) {
227        assert cls != null;
228        try {
229          return cls.newInstance();
230        }
231        catch (InstantiationException e) {
232          DispatcherConfigurationException ex = DispatcherConfigurationException.forUnableToInstantiateDispatcher(cls, e);
233          logger.fatal( ex.getMessage(), ex);
234          throw ex;
235        }
236        catch (IllegalAccessException e) {
237          DispatcherConfigurationException ex = DispatcherConfigurationException.forUnableToInstantiateDispatcher(cls, e);
238          logger.fatal( ex.getMessage(), ex);
239          throw ex;
240        }     
241      }
242      
243      protected void performCustomRegistryConfiguration(Class<? extends ServletRegistryConfigurator> configuratorClass, Registry registry, ServletConfig config) {
244        ServletRegistryConfigurator registryConfigurator = createCustomRegistryConfigurator( configuratorClass ); // registry.getGlobalConfiguration().getRegistryConfiguratorClass() ).configure( registry, configuration);
245        registryConfigurator.configure(registry, config);
246      }
247      
248      private ServletRegistryConfigurator createCustomRegistryConfigurator( Class<? extends ServletRegistryConfigurator> configuratorClass ) {
249        assert configuratorClass != null;
250        
251        try {
252          return configuratorClass.newInstance();
253        }
254        catch (InstantiationException e) {
255          RegistryConfigurationException ex = RegistryConfigurationException.forUnableToInstantiateRegistryConfigurator(configuratorClass, e);
256          logger.fatal( ex.getMessage(), ex);
257          throw ex;
258        }
259        catch (IllegalAccessException e) {
260          RegistryConfigurationException ex = RegistryConfigurationException.forUnableToInstantiateRegistryConfigurator(configuratorClass, e);
261          logger.fatal( ex.getMessage(), ex);
262          throw ex;
263        }     
264      }
265    
266      protected GlobalConfiguration createGlobalConfiguration(ServletConfig configuration) {
267        assert configuration != null;
268        
269        ServletUtils.checkRequiredParameters(configuration, GlobalParameters.PROVIDERS_URL);
270        
271        boolean isDebug = ServletUtils.getBooleanParameter( configuration, GlobalParameters.DEBUG, GlobalConfiguration.DEFAULT_DEBUG_VALUE);
272        String providersUrl = ServletUtils.getRequiredParameter(configuration, GlobalParameters.PROVIDERS_URL);
273        String gsonConfiguratorClassName = ServletUtils.getParameter(configuration, GlobalParameters.GSON_BUILDER_CONFIGURATOR_CLASS, GlobalConfiguration.DEFAULT_GSON_BUILDER_CONFIGURATOR_CLASS.getName());
274        String dispatcherClassName = ServletUtils.getParameter(configuration, GlobalParameters.DISPATCHER_CLASS, /*GlobalConfiguration.DEFAULT_DISPATCHER_CLASS.getName()*/ getDefaultDispatcherClass().getName());    
275        String jsonRequestProcessorThreadClassName = ServletUtils.getParameter(configuration, GlobalParameters.JSON_REQUEST_PROCESSOR_THREAD_CLASS, /*GlobalConfiguration.DEFAULT_JSON_REQUEST_PROCESSOR_THREAD_CLASS.getName()*/ getDefaultJsonRequestProcessoThreadClass().getName());    
276        
277        // Global multithreaded-batched requests support parameters
278        boolean isBatchRequestsMultithreadingEnabled = ServletUtils.getBooleanParameter( configuration, GlobalParameters.BATCH_REQUESTS_MULTITHREADING_ENABLED, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MULTITHREADING_ENABLED_VALUE);
279        boolean minifyEnabled = ServletUtils.getBooleanParameter( configuration, GlobalParameters.MINIFY, GlobalConfiguration.DEFAULT_MINIFY_VALUE);
280        
281        int batchRequestsMinThreadsPoolSize = ServletUtils.getIntParameterGreaterOrEqualToValue(
282           configuration, GlobalParameters.BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE, 
283           GlobalConfiguration.MIN_BATCH_REQUESTS_MIN_THREAD_POOL_SIZE, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MIN_THREAD_POOL_SIZE);
284        int batchRequestsMaxThreadsPoolSize = ServletUtils.getIntParameterGreaterOrEqualToValue(
285            configuration, GlobalParameters.BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE, 
286            GlobalConfiguration.MIN_BATCH_REQUESTS_MAX_THREAD_POOL_SIZE, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MAX_THREAD_POOL_SIZE);
287        int batchRequestsThreadKeepAliveSeconds = ServletUtils.getIntParameterGreaterOrEqualToValue(
288            configuration, GlobalParameters.BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS, 
289            GlobalConfiguration.MIN_BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS);
290        int batchRequestsMaxThreadsPerRequest = ServletUtils.getIntParameterGreaterOrEqualToValue(
291            configuration, GlobalParameters.BATCH_REQUESTS_MAX_THREADS_PER_REQUEST, 
292            GlobalConfiguration.MIN_BATCH_REQUESTS_MAX_THREADS_PER_REQUEST, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MAX_THREADS_PER_REQUEST);
293        String contextPath = configuration.getInitParameter( GlobalParameters.CONTEXT_PATH );
294        boolean createSourceFiles = ServletUtils.getBooleanParameter(configuration, GlobalParameters.CREATE_SOURCE_FILES, GlobalConfiguration.DEFAULT_CREATE_SOURCE_FILES);
295        
296        if( batchRequestsMinThreadsPoolSize > batchRequestsMaxThreadsPoolSize ) {
297          ServletConfigurationException ex = ServletConfigurationException.forMaxThreadPoolSizeMustBeEqualOrGreaterThanMinThreadPoolSize(batchRequestsMinThreadsPoolSize, batchRequestsMaxThreadsPoolSize);
298          logger.fatal( ex.getMessage(), ex );
299          throw ex;
300        }
301        
302        if( logger.isInfoEnabled() ) {
303          String contextPathInfo = contextPath;
304          if( contextPathInfo == null ) {
305            contextPathInfo = "--not specified: calculated via Javascript--";
306          }
307          logger.info( "Servlet GLOBAL configuration: " + 
308            GlobalParameters.DEBUG + "=" + isDebug + ", " +
309            GlobalParameters.PROVIDERS_URL + "=" + providersUrl + ", " +
310            GlobalParameters.MINIFY + "=" + minifyEnabled + ", " +
311            GlobalParameters.BATCH_REQUESTS_MULTITHREADING_ENABLED + "=" + isBatchRequestsMultithreadingEnabled + ", " + 
312            GlobalParameters.BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE + "=" + batchRequestsMinThreadsPoolSize + ", " + 
313            GlobalParameters.BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE + "=" + batchRequestsMaxThreadsPoolSize + ", " + 
314            GlobalParameters.BATCH_REQUESTS_MAX_THREADS_PER_REQUEST + "=" + batchRequestsMaxThreadsPerRequest + ", " + 
315            GlobalParameters.BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS + "=" + batchRequestsThreadKeepAliveSeconds + ", " + 
316            GlobalParameters.GSON_BUILDER_CONFIGURATOR_CLASS + "=" + gsonConfiguratorClassName + ", " +
317            GlobalParameters.DISPATCHER_CLASS + "=" + dispatcherClassName + ", " +
318            GlobalParameters.JSON_REQUEST_PROCESSOR_THREAD_CLASS + "=" + jsonRequestProcessorThreadClassName + ", " +
319            GlobalParameters.CONTEXT_PATH + "=" + contextPathInfo + ", " +
320            GlobalParameters.CREATE_SOURCE_FILES + "=" + createSourceFiles
321          );
322        }
323        
324        Class<? extends GsonBuilderConfigurator> gsonConfiguratorClass = getGsonBuilderConfiguratorClass(gsonConfiguratorClassName);
325        Class<? extends Dispatcher> dispatcherClass = getDispatcherClass(dispatcherClassName);
326        Class<? extends JsonRequestProcessorThread> jsonRequestProcessorClass = getJsonRequestProcessorThreadClass(jsonRequestProcessorThreadClassName);
327        
328        GlobalConfiguration result = new GlobalConfiguration( 
329          contextPath,
330          providersUrl, isDebug, 
331          gsonConfiguratorClass, jsonRequestProcessorClass, dispatcherClass,
332          minifyEnabled, 
333          isBatchRequestsMultithreadingEnabled, batchRequestsMinThreadsPoolSize, batchRequestsMaxThreadsPoolSize,
334          batchRequestsThreadKeepAliveSeconds, batchRequestsMaxThreadsPerRequest,
335          createSourceFiles);
336        return result;
337      }
338    
339      private Class<? extends JsonRequestProcessorThread> getDefaultJsonRequestProcessoThreadClass() {
340        return SsmJsonRequestProcessorThread.class;
341      }
342    
343      private Class<? extends Dispatcher> getDefaultDispatcherClass() {
344        return SsmDispatcher.class;
345      }
346    
347      @SuppressWarnings("unchecked")
348      private Class<? extends GsonBuilderConfigurator> getGsonBuilderConfiguratorClass(String gsonConfiguratorClassName) {
349        assert !StringUtils.isEmpty(gsonConfiguratorClassName);
350        
351        Class<? extends GsonBuilderConfigurator> configuratorClass;
352        try {
353          configuratorClass = (Class<GsonBuilderConfigurator>)Class.forName(gsonConfiguratorClassName);
354          if( !GsonBuilderConfigurator.class.isAssignableFrom(configuratorClass)) {
355            ServletConfigurationException ex = ServletConfigurationException.forGsonBuilderConfiguratorMustImplementGsonBuilderConfiguratorInterface(gsonConfiguratorClassName ); 
356            logger.fatal( ex.getMessage(), ex );
357            throw ex;
358          }
359          return configuratorClass;
360        }
361        catch( ClassNotFoundException ex ) {
362          ServletConfigurationException e = ServletConfigurationException.forClassNotFound(gsonConfiguratorClassName, ex ); 
363          logger.fatal( e.getMessage(), e );
364          throw e;
365        }
366      }
367    
368      @SuppressWarnings("unchecked")
369      private Class<? extends Dispatcher> getDispatcherClass(String dispatcherClassName) {
370        assert !StringUtils.isEmpty(dispatcherClassName);
371        
372        Class<? extends Dispatcher> configuratorClass;
373        try {
374          configuratorClass = (Class<Dispatcher>)Class.forName(dispatcherClassName);
375          if( !Dispatcher.class.isAssignableFrom(configuratorClass)) {
376            ServletConfigurationException ex = ServletConfigurationException.forDispatcherMustImplementDispatcherInterface(dispatcherClassName ); 
377            logger.fatal( ex.getMessage(), ex );
378            throw ex;
379          }
380          return configuratorClass;
381        }
382        catch( ClassNotFoundException ex ) {
383          ServletConfigurationException e = ServletConfigurationException.forClassNotFound(dispatcherClassName, ex ); 
384          logger.fatal( e.getMessage(), e );
385          throw e;
386        }
387      }
388    
389      @SuppressWarnings("unchecked")
390      private Class<? extends JsonRequestProcessorThread> getJsonRequestProcessorThreadClass(String jsonRequestProcessorThreadClassName) {
391        assert !StringUtils.isEmpty(jsonRequestProcessorThreadClassName);
392        
393        Class<? extends JsonRequestProcessorThread> cls;
394        try {
395          cls = (Class<JsonRequestProcessorThread>)Class.forName(jsonRequestProcessorThreadClassName);
396          if( !JsonRequestProcessorThread.class.isAssignableFrom(cls)) {
397            ServletConfigurationException ex = ServletConfigurationException.forJsonRequestProcessorThreadMustImplementJsonRequestProcessorThreadInterface(jsonRequestProcessorThreadClassName ); 
398            logger.fatal( ex.getMessage(), ex );
399            throw ex;
400          }
401          return cls;
402        }
403        catch( ClassNotFoundException ex ) {
404          ServletConfigurationException e = ServletConfigurationException.forClassNotFound(jsonRequestProcessorThreadClassName, ex ); 
405          logger.fatal( e.getMessage(), e );
406          throw e;
407        }
408      }
409    
410      @SuppressWarnings("unchecked")
411      @CheckForNull private Class<? extends ServletRegistryConfigurator> getRegistryConfiguratorClass(String registryConfiguratorClassName) {
412        if( StringUtils.isEmpty(registryConfiguratorClassName)) {
413          return null;
414        }
415        
416        Class<? extends ServletRegistryConfigurator> configuratorClass;
417        try {
418          configuratorClass = (Class<ServletRegistryConfigurator>)Class.forName(registryConfiguratorClassName);
419          if( !ServletRegistryConfigurator.class.isAssignableFrom(configuratorClass)) {
420            ServletConfigurationException ex = ServletConfigurationException.forRegistryConfiguratorMustImplementGsonBuilderConfiguratorInterface(registryConfiguratorClassName ); 
421            logger.fatal( ex.getMessage(), ex );
422            throw ex;
423          }
424          return configuratorClass;
425        }
426        catch( ClassNotFoundException ex ) {
427          ServletConfigurationException e = ServletConfigurationException.forClassNotFound(registryConfiguratorClassName, ex ); 
428          logger.fatal( e.getMessage(), e );
429          throw e;
430        }
431      }
432    
433      protected List<ApiConfiguration> createApiConfigurationsFromServletConfigurationApi(ServletConfig configuration) {
434        assert configuration != null;
435    
436        List<ApiConfiguration> result = new ArrayList<ApiConfiguration>();    
437        String apisParameter = ServletUtils.getRequiredParameter(configuration, GlobalParameters.APIS_PARAMETER);
438        List<String> apis = StringUtils.getNonBlankValues(apisParameter, VALUES_SEPARATOR);
439        logger.info( "Servlet APIs configuration: " + GlobalParameters.APIS_PARAMETER + "=" + apisParameter ); 
440        
441        for( String api : apis) {
442          ApiConfiguration apiConfiguration = createApiConfigurationFromServletConfigurationApi( configuration, api );
443          result.add( apiConfiguration );
444        }
445        
446        if( result.isEmpty() ) {
447          logger.warn( "No apis specified");
448        }
449        
450        return result;
451      }
452    
453      private ApiConfiguration createApiConfigurationFromServletConfigurationApi(ServletConfig configuration, String api) {
454        assert configuration != null;
455        assert !StringUtils.isEmpty(api);
456        
457        String apiFile = ServletUtils.getParameter( configuration, api + "." + ApiParameters.API_FILE, api + ApiConfiguration.DEFAULT_API_FILE_SUFFIX );
458        String fullGeneratedApiFile = getServletContext().getRealPath(apiFile);    
459        String apiNamespace = ServletUtils.getParameter( configuration, api + "." + ApiParameters.API_NAMESPACE, "" );
460        assert apiNamespace != null;
461        String actionsNamespace = ServletUtils.getParameter( configuration, api + "." + ApiParameters.ACTIONS_NAMESPACE, "" );
462    
463        // If apiNamespace is empty, try to use actionsNamespace: if still empty, use the api name itself
464        if( apiNamespace.equals("")) {
465          if( actionsNamespace.equals("")) {
466            apiNamespace = ApiConfiguration.DEFAULT_NAMESPACE_PREFIX + api;
467            if( logger.isDebugEnabled() ) {
468              logger.debug( "Using the api name, prefixed with '" + ApiConfiguration.DEFAULT_NAMESPACE_PREFIX + "' as the value for " + ApiParameters.API_NAMESPACE);          
469            }
470          }
471          else {
472            apiNamespace = actionsNamespace;
473            logger.debug( "Using " + ApiParameters.ACTIONS_NAMESPACE + " as the value for " + ApiParameters.API_NAMESPACE);                  
474          }
475        }
476        
477        String classNames = ServletUtils.getParameter( configuration, api + "." + ApiParameters.CLASSES, "" );
478        List<Class<?>> classes = getClasses( classNames );
479        
480        if( logger.isInfoEnabled() ) {
481          logger.info( "Servlet '" + api + "' Api configuration: " +
482            ApiParameters.API_NAMESPACE + "=" + apiNamespace + ", " +
483            ApiParameters.ACTIONS_NAMESPACE + "=" + actionsNamespace + ", " +
484            ApiParameters.API_FILE + "=" + apiFile + " => Full api file: " + fullGeneratedApiFile + ", " +
485            ApiParameters.CLASSES + "=" + classNames);
486        }
487        
488        if( classes.isEmpty() ) {
489          logger.warn( "There are no action classes to register for api '" + api + "'");
490        }
491        ApiConfiguration apiConfiguration = new ApiConfiguration( api, apiFile, fullGeneratedApiFile, apiNamespace, actionsNamespace, classes );
492            
493        return apiConfiguration;
494      }
495    
496      private static List<Class<?>> getClasses( String classes )  {
497        assert classes != null;
498        
499        List<Class<?>> result = new ArrayList<Class<?>>();
500        if( StringUtils.isEmpty(classes) ) {
501          return result;
502        }
503        List<String> classNames = StringUtils.getNonBlankValues( classes, VALUES_SEPARATOR );
504        
505        for( String className : classNames ) {
506          try {
507            Class<?> cls = Class.forName( className );
508            result.add( cls );
509          }
510          catch( ClassNotFoundException ex ) {
511            logger.fatal( ex.getMessage(), ex );
512            ServletConfigurationException e = ServletConfigurationException.forClassNotFound(className, ex ); 
513            throw e;
514          }
515        }
516        
517        return result;
518      }
519      
520      private static RequestType getFromRequestContentType( HttpServletRequest request ) {
521        assert request != null;
522        
523        String contentType = request.getContentType();
524        String pathInfo = request.getPathInfo();
525        
526        if( !StringUtils.isEmpty(pathInfo) && pathInfo.startsWith( PollRequestProcessor.PATHINFO_POLL_PREFIX)) {
527          return RequestType.POLL;
528        }
529        else if( StringUtils.startsWithCaseInsensitive( contentType, "application/json") ) {
530          return RequestType.JSON;
531        }
532        else if( StringUtils.startsWithCaseInsensitive( contentType, "application/x-www-form-urlencoded") && request.getMethod().equalsIgnoreCase("post")) {
533          return RequestType.FORM_SIMPLE_POST;
534        }
535        else if( ServletFileUpload.isMultipartContent(request)) {
536          return RequestType.FORM_UPLOAD_POST;
537        }
538        else if( RequestRouter.isSourceRequest(pathInfo)) {
539          return RequestType.SOURCE;
540        }
541        else {
542          String requestInfo = ServletUtils.getDetailedRequestInformation(request);      
543          RequestException ex = RequestException.forRequestFormatNotRecognized();
544          logger.error( "Error during file uploader: " + ex.getMessage() + "\nAdditional request information: " + requestInfo, ex );
545          throw ex;
546        }
547      }
548    
549      @Override
550      public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
551        assert request != null;
552        assert response != null;
553        doPost(request, response);
554      }
555    
556      @Override
557      public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
558        assert request != null;
559        assert response != null;
560    
561        NDC.push( "rid=" + Long.toString(getUniqueRequestId()) );
562        try {
563          Timer timer = new Timer();
564          try {
565            attachThreadLocalData( request, response);
566            try {
567              if( logger.isTraceEnabled()) {
568                String requestInfo = ServletUtils.getDetailedRequestInformation(request);
569                logger.trace( "Request info: " + requestInfo);
570              }        
571    
572              String requestEncoding = request.getCharacterEncoding();
573              // If we don't know what the request encoding is, assume it to be UTF-8
574              if( StringUtils.isEmpty(requestEncoding)) {
575                request.setCharacterEncoding(EncodingUtils.UTF8);
576              }
577              response.setCharacterEncoding(EncodingUtils.UTF8);
578    
579              RequestType type = getFromRequestContentType(request);
580              processRequest(request, response, type);
581            }
582            finally {
583              detachThreadLocalData();
584            }
585          }
586          finally {
587            timer.stop();
588            timer.logDebugTimeInMilliseconds("Total servlet processing time");
589          }
590    
591        }
592        finally {
593          NDC.pop();
594        }
595      }
596    
597      @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="SF_SWITCH_NO_DEFAULT",
598          justification="Missing a 'default' branch is not a problem with enums, given the appropriate compiler settings")
599      private void processRequest(HttpServletRequest request, HttpServletResponse response, RequestType type)
600          throws IOException {
601       final String JSON_CONTENT_TYPE = "application/json";
602       final String JAVASCRIPT_CONTENT_TYPE = "text/javascript"; // *YES*, shoul be "application/javascript", but then there is IE, and the fact that this is really cross-browser (sigh!)
603       final String HTML_CONTENT_TYPE = "text/html";
604        
605       RequestRouter processor = getProcessor();
606        switch( type ) {    
607        case FORM_SIMPLE_POST:
608          response.setContentType(JSON_CONTENT_TYPE); 
609          processor.processSimpleFormPostRequest( request.getReader(), response.getWriter() );
610          break;
611        case FORM_UPLOAD_POST:
612          response.setContentType(HTML_CONTENT_TYPE); // MUST be "text/html" for uploads to work!
613          processUploadFormPost(request, response);
614          break;
615        case JSON:
616          response.setContentType(JSON_CONTENT_TYPE); 
617          processor.processJsonRequest( request.getReader(), response.getWriter() );
618          break;
619        case POLL:
620          response.setContentType(JSON_CONTENT_TYPE); 
621          processor.processPollRequest( request.getReader(), response.getWriter(), request.getPathInfo() );
622          break;
623        case SOURCE:
624          response.setContentType(JAVASCRIPT_CONTENT_TYPE); 
625          processor.processSourceRequest( request.getReader(), response.getWriter(), request.getPathInfo());
626          break;
627        }
628      }
629    
630      protected void attachThreadLocalData( HttpServletRequest request, HttpServletResponse response) {
631        WebContextManager.initializeWebContextForCurrentThread(this, request, response);
632      }
633    
634      protected void detachThreadLocalData() {
635        WebContextManager.detachFromCurrentThread();
636      }
637      
638      private void processUploadFormPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
639        assert request != null;
640        assert response != null;
641        
642        RequestRouter router = getProcessor();
643        UploadFormPostRequestProcessor processor = router.createUploadFromProcessor();
644        try {
645           router.processUploadFormPostRequest( processor, getFileItems(request), response.getWriter() );
646        }
647        catch( FileUploadException e ) {
648          processor.handleFileUploadException( e );
649        }
650      }
651    
652      @SuppressWarnings("unchecked")
653      private List<FileItem> getFileItems(HttpServletRequest request) throws FileUploadException {
654        assert request != null;
655    
656        ServletFileUpload uploader = getUploader();
657        return uploader.parseRequest(request);
658      }
659    
660    }