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 }