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 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 General Public License for more details.
018     *
019     * You should have received a copy of the GNU 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.List;
031    
032    import javax.servlet.ServletConfig;
033    import javax.servlet.ServletException;
034    import javax.servlet.http.HttpServlet;
035    import javax.servlet.http.HttpServletRequest;
036    import javax.servlet.http.HttpServletResponse;
037    
038    import org.apache.commons.fileupload.FileItem;
039    import org.apache.commons.fileupload.FileUploadException;
040    import org.apache.commons.fileupload.servlet.ServletFileUpload;
041    import org.apache.log4j.Logger;
042    import org.apache.log4j.NDC;
043    
044    import com.softwarementors.extjs.djn.EncodingUtils;
045    import com.softwarementors.extjs.djn.ServletUtils;
046    import com.softwarementors.extjs.djn.StringUtils;
047    import com.softwarementors.extjs.djn.Timer;
048    import com.softwarementors.extjs.djn.api.Registry;
049    import com.softwarementors.extjs.djn.config.ApiConfiguration;
050    import com.softwarementors.extjs.djn.config.GlobalConfiguration;
051    import com.softwarementors.extjs.djn.gson.GsonBuilderConfigurator;
052    import com.softwarementors.extjs.djn.jscodegen.CodeFileGenerator;
053    import com.softwarementors.extjs.djn.router.RequestRouter;
054    import com.softwarementors.extjs.djn.router.RequestType;
055    import com.softwarementors.extjs.djn.router.processor.RequestException;
056    import com.softwarementors.extjs.djn.router.processor.poll.PollRequestProcessor;
057    import com.softwarementors.extjs.djn.router.processor.standard.form.upload.UploadFormPostRequestProcessor;
058    
059    public class DirectJNgineServlet extends HttpServlet {
060    
061      private static final Logger logger = Logger.getLogger( DirectJNgineServlet.class);
062    
063      /*********************************************************  
064       * GlobalParameters and configuration
065       *********************************************************/
066      private static final String VALUES_SEPARATOR = ",";
067    
068      public static class GlobalParameters {
069        public static final String PROVIDERS_URL = "providersUrl";
070        public static final String DEBUG = "debug";
071        public static final String GSON_BUILDER_CONFIGURATOR_CLASS = "gsonBuilderConfiguratorClass";
072        
073        private static final String APIS_PARAMETER = "apis";
074        private static final String MINIFY = "minify";
075    
076        public static final String BATCH_REQUESTS_MULTITHREADING_ENABLED = "batchRequestsMultithreadingEnabled";
077        public static final String BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE = "batchRequestsMinThreadsPoolSize";
078        public static final String BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE = "batchRequestsMaxThreadsPoolSize";
079        public static final String BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS = "batchRequestsMaxThreadKeepAliveSeconds";
080        public static final String BATCH_REQUESTS_MAX_THREADS_PER_REQUEST = "batchRequestsMaxThreadsPerRequest";
081      }
082    
083      public static class ApiParameters {
084        public static final String API_FILE = "apiFile";
085        public static final String API_NAMESPACE = "apiNamespace";
086        public static final String ACTIONS_NAMESPACE = "actionsNamespace";
087        public static final String CLASSES = "classes";
088      }
089    
090      private Registry registry;
091      private GlobalConfiguration globalConfiguration;
092      private RequestRouter processor;
093      private ServletFileUpload upload;
094      private long id = 1000; // It is good we will get lots of ids with the same number of digits...
095      private synchronized long getUniqueRequestId() {
096        return this.id++;
097      }
098      
099      
100      @Override
101      public void init(ServletConfig configuration) throws ServletException
102      {
103        assert configuration != null;
104        super.init(configuration);
105        
106        createDirectJNgineRouter(configuration);
107      }
108    
109    
110      protected void createDirectJNgineRouter(ServletConfig configuration) throws ServletException {
111        Timer timer = new Timer();
112    
113        Timer subtaskTimer = new Timer();
114        this.globalConfiguration = createGlobalConfiguration(configuration);    
115        List<ApiConfiguration> apiConfigurations = createApiConfigurations(configuration);
116        subtaskTimer.stop();
117        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Servlet Configuration Parameters load time");
118        
119        subtaskTimer.restart();
120        this.registry = new Registry( this.globalConfiguration, apiConfigurations);
121        subtaskTimer.stop();
122        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Method Registration time");
123          
124        subtaskTimer.restart();
125        try {
126          CodeFileGenerator.updateApiFiles(this.registry);
127          subtaskTimer.stop();
128          subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Api Files creation time");
129        }
130        catch( IOException ex ) {
131          ServletException e = new ServletException( "Unable to create DirectJNgine API files",  ex );
132          logger.fatal( e.getMessage(), e );
133           throw e;
134        }
135        
136        subtaskTimer.restart();
137        this.upload = UploadFormPostRequestProcessor.createFileUploader();
138        this.processor = createRequestProcessor();
139        subtaskTimer.stop();
140        subtaskTimer.logDebugTimeInMilliseconds("Djn initialization: Request Processor initialization time");
141        
142        timer.stop();
143        timer.logDebugTimeInMilliseconds("Djn initialization: total DirectJNgine initialization time");
144      }
145      
146      private RequestRouter createRequestProcessor() {
147        return new RequestRouter( this.registry, this.globalConfiguration );    
148      }
149     
150      protected GlobalConfiguration createGlobalConfiguration(ServletConfig configuration) {
151        assert configuration != null;
152        
153        ServletUtils.checkRequiredParameters(configuration, GlobalParameters.PROVIDERS_URL);
154        
155        boolean isDebug = ServletUtils.getBooleanParameter( configuration, GlobalParameters.DEBUG, GlobalConfiguration.DEFAULT_DEBUG_VALUE);
156        String providersUrl = ServletUtils.getRequiredParameter(configuration, GlobalParameters.PROVIDERS_URL);
157        String gsonConfiguratorClassName = ServletUtils.getParameter(configuration, GlobalParameters.GSON_BUILDER_CONFIGURATOR_CLASS, GlobalConfiguration.DEFAULT_GSON_BUILDER_CONFIGURATOR_CLASS.getName());
158        Class<? extends GsonBuilderConfigurator> configuratorClass = getGsonBuilderConfiguratorClass(gsonConfiguratorClassName);
159        
160        // Global multithreaded-batched requests support parameters
161        boolean isBatchRequestsMultithreadingEnabled = ServletUtils.getBooleanParameter( configuration, GlobalParameters.BATCH_REQUESTS_MULTITHREADING_ENABLED, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MULTITHREADING_ENABLED_VALUE);
162        boolean minifyEnabled = ServletUtils.getBooleanParameter( configuration, GlobalParameters.MINIFY, GlobalConfiguration.DEFAULT_MINIFY_VALUE);
163        
164        int batchRequestsMinThreadsPoolSize = ServletUtils.getIntParameterGreaterOrEqualToValue(
165           configuration, GlobalParameters.BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE, 
166           GlobalConfiguration.MIN_BATCH_REQUESTS_MIN_THREAD_POOL_SIZE, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MIN_THREAD_POOL_SIZE);
167        int batchRequestsMaxThreadsPoolSize = ServletUtils.getIntParameterGreaterOrEqualToValue(
168            configuration, GlobalParameters.BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE, 
169            GlobalConfiguration.MIN_BATCH_REQUESTS_MAX_THREAD_POOL_SIZE, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MAX_THREAD_POOL_SIZE);
170        int batchRequestsThreadKeepAliveSeconds = ServletUtils.getIntParameterGreaterOrEqualToValue(
171            configuration, GlobalParameters.BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS, 
172            GlobalConfiguration.MIN_BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS);
173        int batchRequestsMaxThreadsPerRequest = ServletUtils.getIntParameterGreaterOrEqualToValue(
174            configuration, GlobalParameters.BATCH_REQUESTS_MAX_THREADS_PER_REQUEST, 
175            GlobalConfiguration.MIN_BATCH_REQUESTS_MAX_THREADS_PER_REQUEST, GlobalConfiguration.DEFAULT_BATCH_REQUESTS_MAX_THREADS_PER_REQUEST);
176        
177        if( batchRequestsMinThreadsPoolSize > batchRequestsMaxThreadsPoolSize ) {
178          ServletConfigurationException ex = ServletConfigurationException.forMaxThreadPoolSizeMustBeEqualOrGreaterThanMinThreadPoolSize(batchRequestsMinThreadsPoolSize, batchRequestsMaxThreadsPoolSize);
179          logger.fatal( ex.getMessage(), ex );
180          throw ex;
181        }
182        
183        if( logger.isInfoEnabled() ) {
184          logger.info( "Servlet GLOBAL configuration: " + 
185            GlobalParameters.DEBUG + "=" + isDebug + ", " +
186            GlobalParameters.PROVIDERS_URL + "=" + providersUrl + ", " +
187            GlobalParameters.GSON_BUILDER_CONFIGURATOR_CLASS + "=" + gsonConfiguratorClassName + ", " +
188            GlobalParameters.MINIFY + "=" + minifyEnabled + ", " +
189            GlobalParameters.BATCH_REQUESTS_MULTITHREADING_ENABLED + "=" + isBatchRequestsMultithreadingEnabled + ", " + 
190            GlobalParameters.BATCH_REQUESTS_MIN_THREADS_POOOL_SIZE + "=" + batchRequestsMinThreadsPoolSize + ", " + 
191            GlobalParameters.BATCH_REQUESTS_MAX_THREADS_POOOL_SIZE + "=" + batchRequestsMaxThreadsPoolSize + ", " + 
192            GlobalParameters.BATCH_REQUESTS_MAX_THREADS_PER_REQUEST + "=" + batchRequestsMaxThreadsPerRequest + ", " + 
193            GlobalParameters.BATCH_REQUESTS_THREAD_KEEP_ALIVE_SECONDS + "=" + batchRequestsThreadKeepAliveSeconds 
194          );
195        }
196        
197        GlobalConfiguration result = new GlobalConfiguration( providersUrl, isDebug, configuratorClass, minifyEnabled,
198          isBatchRequestsMultithreadingEnabled, batchRequestsMinThreadsPoolSize, batchRequestsMaxThreadsPoolSize,
199          batchRequestsThreadKeepAliveSeconds, batchRequestsMaxThreadsPerRequest );
200        return result;
201      }
202    
203      private Class<? extends GsonBuilderConfigurator> getGsonBuilderConfiguratorClass(String gsonConfiguratorClassName) {
204        Class<? extends GsonBuilderConfigurator> configuratorClass;
205        try {
206          configuratorClass = loadGsonConfiguratorClass(gsonConfiguratorClassName);
207          if( !GsonBuilderConfigurator.class.isAssignableFrom(configuratorClass)) {
208            ServletConfigurationException ex = ServletConfigurationException.forGsonBuilderConfiguratorMustImplementGsonBuilderConfiguratorInterface(gsonConfiguratorClassName ); 
209            logger.fatal( ex.getMessage(), ex );
210            throw ex;
211          }
212          return configuratorClass;
213        }
214        catch( ClassNotFoundException ex ) {
215          ServletConfigurationException e = ServletConfigurationException.forClassNotFound(gsonConfiguratorClassName, ex ); 
216          logger.fatal( e.getMessage(), e );
217          throw e;
218        }
219      }
220    
221      @SuppressWarnings("unchecked") // Avoid generics typecast warning
222      private Class<GsonBuilderConfigurator> loadGsonConfiguratorClass(String gsonConfiguratorClassName)
223          throws ClassNotFoundException {
224        return (Class<GsonBuilderConfigurator>)Class.forName(gsonConfiguratorClassName);
225      }
226    
227      protected List<ApiConfiguration> createApiConfigurations(ServletConfig configuration) {
228        assert configuration != null;
229    
230        ServletUtils.checkRequiredParameters(configuration, GlobalParameters.APIS_PARAMETER);
231        String apisParameter = ServletUtils.getRequiredParameter(configuration, GlobalParameters.APIS_PARAMETER);
232        List<String> apis = StringUtils.getNonBlankValues(apisParameter, VALUES_SEPARATOR);
233        logger.info( "Servlet APIs configuration: " + GlobalParameters.APIS_PARAMETER + "=" + apisParameter ); 
234        
235        if( apis.isEmpty() ) {
236          logger.warn( "No apis specified");
237        }
238        
239        List<ApiConfiguration> result = new ArrayList<ApiConfiguration>();    
240        for( String api : apis) {
241          ApiConfiguration apiConfiguration = createApiConfiguration( configuration, api );
242          result.add( apiConfiguration );
243        }
244        
245        return result;
246      }
247    
248      private ApiConfiguration createApiConfiguration(ServletConfig configuration, String api) {
249        assert configuration != null;
250        assert !StringUtils.isEmpty(api);
251        
252        String apiFile = ServletUtils.getParameter( configuration, api + "." + ApiParameters.API_FILE, api + ApiConfiguration.DEFAULT_API_FILE_SUFFIX );
253        String fullGeneratedApiFile = getServletContext().getRealPath(apiFile);    
254        String apiNamespace = ServletUtils.getParameter( configuration, api + "." + ApiParameters.API_NAMESPACE, "" );
255        String actionsNamespace = ServletUtils.getParameter( configuration, api + "." + ApiParameters.ACTIONS_NAMESPACE, "" );
256    
257        // If apiNamespace is empty, try to use actionsNamespace: if still empty, use the api name itself
258        if( apiNamespace.equals("")) {
259          if( actionsNamespace.equals("")) {
260            apiNamespace = ApiConfiguration.DEFAULT_NAMESPACE_PREFIX + api;
261            if( logger.isDebugEnabled() ) {
262              logger.debug( "Using the api name, prefixed with '" + ApiConfiguration.DEFAULT_NAMESPACE_PREFIX + "' as the value for " + ApiParameters.API_NAMESPACE);          
263            }
264          }
265          else {
266            apiNamespace = actionsNamespace;
267            logger.debug( "Using " + ApiParameters.ACTIONS_NAMESPACE + " as the value for " + ApiParameters.API_NAMESPACE);                  
268          }
269        }
270        
271        String classNames = ServletUtils.getParameter( configuration, api + "." + ApiParameters.CLASSES, "" );
272        List<Class<?>> classes = getClasses( classNames );
273        
274        if( logger.isInfoEnabled() ) {
275          logger.info( "Servlet '" + api + "' API configuration: " +
276            ApiParameters.API_NAMESPACE + "=" + apiNamespace + ", " +
277            ApiParameters.ACTIONS_NAMESPACE + "=" + actionsNamespace + ", " +
278            ApiParameters.API_FILE + "=" + apiFile + " => Api file: " + fullGeneratedApiFile + ", " +
279            ApiParameters.CLASSES + "=" + classNames);
280        }
281        
282        if( classes.isEmpty() ) {
283          logger.warn( "There are no action classes to register for api '" + api + "'");
284        }
285        ApiConfiguration apiConfiguration = new ApiConfiguration( api, fullGeneratedApiFile, apiNamespace, actionsNamespace, classes );
286            
287        return apiConfiguration;
288      }
289    
290      private static List<Class<?>> getClasses( String classes )  {
291        
292        List<Class<?>> result = new ArrayList<Class<?>>();
293        if( StringUtils.isEmpty(classes) ) {
294          return result;
295        }
296        List<String> classNames = StringUtils.getNonBlankValues( classes, VALUES_SEPARATOR );
297        
298        for( String className : classNames ) {
299          try {
300            Class<?> cls = Class.forName( className );
301            result.add( cls );
302          }
303          catch( ClassNotFoundException ex ) {
304            logger.fatal( ex.getMessage(), ex );
305            ServletConfigurationException e = ServletConfigurationException.forClassNotFound(className, ex ); 
306            throw e;
307          }
308        }
309        
310        return result;
311      }
312      
313      private static RequestType getFromRequestContentType( HttpServletRequest request ) {
314        assert request != null;
315        
316        String contentType = request.getContentType();
317        String contentTypeLowercase = "";
318        if( contentType != null ) {
319          contentTypeLowercase = contentType.toLowerCase();
320        }
321        
322        String pathInfo = request.getPathInfo();
323        
324        if( !StringUtils.isEmpty(pathInfo) && pathInfo.startsWith( PollRequestProcessor.PATHINFO_POLL_PREFIX)) {
325          return RequestType.POLL;
326        }
327        else if( contentTypeLowercase.startsWith( "application/json")) {
328          return RequestType.JSON;
329        }
330        else if( contentTypeLowercase.startsWith("application/x-www-form-urlencoded") && request.getMethod().toLowerCase().equals("post")) {
331          return RequestType.FORM_SIMPLE_POST;
332        }
333        else if( ServletFileUpload.isMultipartContent(request)) {
334          return RequestType.FORM_UPLOAD_POST;
335        }
336        else {
337          String requestInfo = ServletUtils.getDetailedRequestInformation(request);      
338          RequestException ex = RequestException.forRequestFormatNotRecognized();
339          logger.error( "Error during file upload: " + ex.getMessage() + "\nAdditional request information: " + requestInfo, ex );
340          throw ex;
341        }
342      }
343    
344      @Override
345      public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
346        doPost(request, response);
347      }
348    
349      @Override
350      public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
351        NDC.push( "rid: " + Long.toString(getUniqueRequestId()) );
352        try {
353          Timer timer = new Timer();
354          try {
355            if( logger.isTraceEnabled()) {
356              String requestInfo = ServletUtils.getDetailedRequestInformation(request);
357              logger.trace( "Request info: " + requestInfo);
358            }        
359    
360            response.setContentType("text/html"); // MUST be "text/html" for uploads to work!
361            String requestEncoding = request.getCharacterEncoding();
362            // If we don't know what the request encoding is, assume it to be UTF-8
363            if( StringUtils.isEmpty(requestEncoding)) {
364              request.setCharacterEncoding(EncodingUtils.UTF8);
365            }
366            response.setCharacterEncoding(EncodingUtils.UTF8);
367    
368            RequestType type = getFromRequestContentType(request);
369            switch( type ) {
370            case FORM_SIMPLE_POST:
371              this.processor.processSimpleFormPostRequest( request.getReader(), response.getWriter() );
372              break;
373            case FORM_UPLOAD_POST:
374              processUploadFormPost(request, response);
375              break;
376            case JSON:
377              this.processor.processJsonRequest( request.getReader(), response.getWriter() );
378              break;
379            case POLL:
380              this.processor.processPollRequest( request.getReader(), response.getWriter(), request.getPathInfo() );
381              break;
382            }
383          }
384          finally {
385            timer.stop();
386            timer.logDebugTimeInMilliseconds("Total servlet processing time");
387          }
388        }
389        finally {
390          NDC.pop();
391        }
392      }
393    
394      private void processUploadFormPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
395        try {
396          this.processor.processUploadFormPostRequest( getFileItems(request), response.getWriter() );
397        }
398        catch( FileUploadException e ) {
399          this.processor.handleFileUploadException( e );
400        }
401      }
402      
403      @SuppressWarnings("unchecked") // Unfortunately, parseRequest returns List, not List<FileItem>
404      private List<FileItem> getFileItems(HttpServletRequest request) throws FileUploadException {
405        return this.upload.parseRequest(request);
406      }
407    
408    }