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 }