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