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.router.processor.standard.json; 027 028 import java.io.IOException; 029 import java.io.Reader; 030 import java.io.Writer; 031 import java.util.ArrayList; 032 import java.util.Collection; 033 import java.util.List; 034 import java.util.concurrent.Callable; 035 import java.util.concurrent.ExecutionException; 036 import java.util.concurrent.ExecutorService; 037 import java.util.concurrent.LinkedBlockingQueue; 038 import java.util.concurrent.ThreadPoolExecutor; 039 import java.util.concurrent.TimeUnit; 040 041 import org.apache.commons.io.IOUtils; 042 import org.apache.log4j.Logger; 043 044 import com.google.gson.JsonArray; 045 import com.google.gson.JsonElement; 046 import com.google.gson.JsonObject; 047 import com.google.gson.JsonParseException; 048 import com.google.gson.JsonParser; 049 import com.google.gson.JsonPrimitive; 050 import com.softwarementors.extjs.djn.ClassUtils; 051 import com.softwarementors.extjs.djn.ParallelTask; 052 import com.softwarementors.extjs.djn.StringUtils; 053 import com.softwarementors.extjs.djn.Timer; 054 import com.softwarementors.extjs.djn.UnexpectedException; 055 import com.softwarementors.extjs.djn.api.RegisteredMethod; 056 import com.softwarementors.extjs.djn.api.Registry; 057 import com.softwarementors.extjs.djn.config.GlobalConfiguration; 058 import com.softwarementors.extjs.djn.gson.JsonException; 059 import com.softwarementors.extjs.djn.router.dispatcher.Dispatcher; 060 import com.softwarementors.extjs.djn.router.processor.RequestException; 061 import com.softwarementors.extjs.djn.router.processor.RequestProcessorBase; 062 import com.softwarementors.extjs.djn.router.processor.ResponseBase; 063 import com.softwarementors.extjs.djn.router.processor.standard.JsonErrorResponse; 064 import com.softwarementors.extjs.djn.router.processor.standard.JsonSuccessResponse; 065 066 public class JsonRequestProcessor extends RequestProcessorBase { 067 068 private static final Logger logger = Logger.getLogger( JsonRequestProcessor.class); 069 private final ExecutorService individualRequestsThreadPool = createThreadPool(); 070 071 private ExecutorService getIndividualRequestsThreadPool() { 072 return this.individualRequestsThreadPool; 073 } 074 075 private ExecutorService createThreadPool() { 076 ExecutorService result = new ThreadPoolExecutor( getGlobalConfiguration().getBatchRequestsMinThreadsPoolSize(), 077 getGlobalConfiguration().getBatchRequestsMaxThreadsPoolSize(), 078 getGlobalConfiguration().getBatchRequestsThreadKeepAliveSeconds(), 079 TimeUnit.SECONDS, 080 new LinkedBlockingQueue<Runnable>()); 081 return result; 082 } 083 084 public JsonRequestProcessor(Registry registry, Dispatcher dispatcher, GlobalConfiguration globalConfiguration) { 085 super(registry, dispatcher, globalConfiguration); 086 } 087 088 089 public String process(Reader reader, Writer writer) throws IOException { 090 String requestString = IOUtils.toString(reader); 091 if( logger.isDebugEnabled() ) { 092 logger.debug( "Request data (JSON)=>" + requestString ); 093 } 094 095 JsonRequest[] requests = getIndividualJsonRequests( requestString ); 096 final boolean isBatched = requests.length > 1; 097 if( isBatched ) { 098 if( logger.isDebugEnabled() ) { 099 logger.debug( "Batched request: " + requests.length + " individual requests batched"); 100 } 101 } 102 103 Collection<ResponseBase> responses = null; 104 boolean useMultipleThreadsIfBatched = isBatched && getGlobalConfiguration().getBatchRequestsMultithreadingEnabled(); 105 if( useMultipleThreadsIfBatched ) { 106 responses = processIndividualRequestsInMultipleThreads( requests); 107 } 108 else { 109 responses = processIndividualRequestsInThisThread(requests); 110 } 111 112 String result = convertInvididualResponsesToJsonString( responses); 113 writer.write( result ); 114 if( logger.isDebugEnabled() ) { 115 logger.debug( "Response data (JSON)=>" + result ); 116 } 117 return result; 118 } 119 120 121 private Collection<ResponseBase> processIndividualRequestsInThisThread(JsonRequest[] requests) { 122 Collection<ResponseBase> responses; 123 boolean isBatched = requests.length > 1; 124 responses = new ArrayList<ResponseBase>(requests.length); 125 int requestNumber = 1; 126 for( JsonRequest request : requests ) { 127 ResponseBase response = processIndividualRequest( request, isBatched, requestNumber ); 128 responses.add( response ); 129 requestNumber++; 130 } 131 return responses; 132 } 133 134 private Collection<ResponseBase> processIndividualRequestsInMultipleThreads( JsonRequest[] requests) { 135 assert requests != null; 136 137 int individualRequestNumber = 1; 138 Collection<Callable<ResponseBase>> tasks = new ArrayList<Callable<ResponseBase>>(requests.length); 139 for (final JsonRequest request : requests) { 140 class IndividualRequestProcessor implements Callable<ResponseBase> { 141 private int requestNumber; 142 public IndividualRequestProcessor( int requestNumber ) { 143 this.requestNumber = requestNumber; 144 } 145 146 // @Override 147 public ResponseBase call() throws Exception { 148 return processIndividualRequest( request, true, this.requestNumber ); 149 } 150 } 151 152 tasks.add(new IndividualRequestProcessor(individualRequestNumber)); 153 individualRequestNumber++; 154 } 155 156 try { 157 Collection<ResponseBase> responses = new ParallelTask<ResponseBase>( 158 getIndividualRequestsThreadPool(), tasks, getGlobalConfiguration().getBatchRequestsMaxThreadsPerRequest()).get(); 159 return responses; 160 } 161 catch (InterruptedException e) { 162 List<ResponseBase> responses = new ArrayList<ResponseBase>(requests.length); 163 logger.error( "(Controlled) server error for cancelled a batch of " + requests.length + " individual requests due to an InterruptedException exception. " + e.getMessage(), e); 164 for (final JsonRequest request : requests) { 165 JsonErrorResponse response = createJsonServerErrorResponse(request, e); 166 responses.add(response); 167 } 168 return responses; 169 } 170 catch (ExecutionException e) { 171 UnexpectedException ex = UnexpectedException.forExecutionExceptionShouldNotHappenBecauseProcessorHandlesExceptionsAsServerErrorResponses(e); 172 logger.error( ex.getMessage(), ex ); 173 throw ex; 174 } 175 } 176 177 private JsonRequest[] getIndividualJsonRequests( String requestString ) { 178 assert !StringUtils.isEmpty(requestString); 179 180 JsonObject[] individualJsonRequests = parseIndividualJsonRequests(requestString, getJsonParser()); 181 JsonRequest[] individualRequests = new JsonRequest[individualJsonRequests.length]; 182 183 int i = 0; 184 for( JsonObject individualRequest : individualJsonRequests ) { 185 individualRequests[i] = createIndividualJsonRequest(individualRequest); 186 i++; 187 } 188 189 return individualRequests; 190 } 191 192 private String convertInvididualResponsesToJsonString(Collection<ResponseBase> responses) { 193 assert responses != null; 194 assert !responses.isEmpty(); 195 196 StringBuilder result = new StringBuilder(); 197 if( responses.size() > 1 ) { 198 result.append( "[\n" ); 199 } 200 int j = 0; 201 for( ResponseBase response : responses ) { 202 appendIndividualResponseJsonString(response, result); 203 boolean isLast = j == responses.size() - 1; 204 if( !isLast) { 205 result.append( ","); 206 } 207 j++; 208 } 209 if( responses.size() > 1 ) { 210 result.append( "]"); 211 } 212 return result.toString(); 213 } 214 215 private Object[] getIndividualRequestParameters(JsonRequest request) { 216 assert request != null; 217 218 RegisteredMethod method = getMethod( request.getAction(), request.getMethod()); 219 assert method != null; 220 221 Object[] parameters; 222 if( !method.getHandleDataAsJsonArray()) { 223 checkJsonMethodParameterTypes( request.getJsonData(), method ); 224 parameters = jsonStringToMethodParameters(method, request.getJsonData(), method.getParameterTypes(), getDebug()); 225 } 226 else { 227 parameters = new Object[] { getJsonParser().parse(request.getJsonData()) }; 228 } 229 return parameters; 230 } 231 232 private Object[] jsonStringToMethodParameters(RegisteredMethod method, String jsonParametersString, Class<?>[] parameterTypes, boolean debug) { 233 assert debug == debug; // To please our very demanding compiler options! 234 235 assert method != null; 236 assert jsonParametersString != null; 237 assert parameterTypes != null; 238 239 try { 240 JsonElement[] jsonParameters = getJsonElements(jsonParametersString); 241 Object[] result = getMethodParameters(parameterTypes, jsonParameters); 242 return result; 243 } 244 catch( JsonParseException ex ) { 245 throw JsonException.forFailedConversionFromJsonStringToMethodParameters( method, jsonParametersString, parameterTypes, ex); 246 } 247 } 248 249 private JsonElement[] getJsonElements(String jsonParameters) { 250 assert jsonParameters != null; 251 252 JsonElement root = getJsonParser().parse(jsonParameters); 253 if( root.isJsonNull() ) { 254 return new JsonElement[] {}; 255 } 256 257 assert root.isJsonArray(); 258 JsonElement[] parameters; 259 260 JsonArray dataArray = (JsonArray)root; 261 parameters = new JsonElement[dataArray.size()]; 262 for( int i = 0; i < dataArray.size(); i++ ) { 263 parameters[i] = dataArray.get(i); 264 } 265 return parameters; 266 } 267 268 private Object[] getMethodParameters(Class<?>[] parameterTypes, JsonElement[] jsonParameters) { 269 assert parameterTypes != null; 270 assert jsonParameters != null; 271 272 Object[] result = new Object[jsonParameters.length]; 273 for( int i = 0; i < jsonParameters.length; i++ ) { 274 JsonElement jsonValue = jsonParameters[i]; 275 String json = jsonValue.toString(); 276 Object value = getGson().fromJson(json, parameterTypes[i]); 277 result[i] = value; 278 } 279 return result; 280 } 281 282 283 private JsonElement[] getIndividualRequestJsonParameters(JsonParser parser, String jsonParameters) { 284 assert jsonParameters != null; 285 286 JsonElement root = parser.parse(jsonParameters); 287 if( root.isJsonNull() ) { 288 return new JsonElement[] {}; 289 } 290 291 assert root.isJsonArray(); 292 JsonElement[] parameters; 293 294 JsonArray dataArray = (JsonArray)root; 295 parameters = new JsonElement[dataArray.size()]; 296 for( int i = 0; i < dataArray.size(); i++ ) { 297 parameters[i] = dataArray.get(i); 298 } 299 return parameters; 300 } 301 302 private void checkJsonMethodParameterTypes(String jsonData, RegisteredMethod method) { 303 assert !StringUtils.isEmpty( jsonData ); 304 assert method != null; 305 306 JsonElement[] jsonParameters = getIndividualRequestJsonParameters( getJsonParser(), jsonData ); 307 Class<?>[] parameterTypes = method.getParameterTypes(); 308 309 assert jsonParameters.length == parameterTypes.length; 310 311 for( int i = 0; i < parameterTypes.length; i++ ) { 312 Class<?> parameterType = parameterTypes[i]; 313 JsonElement jsonElement = jsonParameters[i]; 314 if( !isValidJsonTypeForJavaType(jsonElement, parameterType )) { 315 throw new IllegalArgumentException( "'" + jsonElement.toString() + "' is not a valid json text for the '" + parameterType.getName() + "' Java type"); 316 } 317 } 318 } 319 320 private boolean isValidJsonTypeForJavaType(JsonElement jsonElement, Class<?> parameterType) { 321 assert jsonElement != null; 322 assert parameterType != null; 323 324 // Check json nulls 325 if( jsonElement.isJsonNull() ) { 326 return !parameterType.isPrimitive(); 327 } 328 329 // Check json arrays 330 if( parameterType.isArray() ) { 331 return jsonElement.isJsonArray(); 332 } 333 334 if( parameterType == Boolean.class || parameterType == boolean.class ) { 335 return jsonElement.isJsonPrimitive() && ((JsonPrimitive)jsonElement).isBoolean(); 336 } 337 else if( parameterType == char.class || parameterType == Character.class) { 338 if( jsonElement.isJsonPrimitive() && ((JsonPrimitive)jsonElement).isString() ) { 339 return jsonElement.getAsString().length() == 1; 340 } 341 return false; 342 } 343 else if( parameterType == String.class ) { 344 return jsonElement.isJsonPrimitive() && ((JsonPrimitive)jsonElement).isString(); 345 } 346 else if( ClassUtils.isNumericType(parameterType)) { 347 return jsonElement.isJsonPrimitive() && ((JsonPrimitive)jsonElement).isNumber(); 348 } 349 350 // If we arrived here, assume somebody will know how to handle the json element, maybe customizing Gson's serialization 351 return true; 352 } 353 354 private ResponseBase processIndividualRequest( JsonRequest request, boolean isBatched, int requestNumber ) { 355 assert request != null; 356 boolean resultReported = false; 357 358 Timer timer = new Timer(); 359 try { 360 if( isBatched ) { 361 if( logger.isDebugEnabled() ) { 362 logger.debug( " - Individual request #" + requestNumber + " request data=>" + getGson().toJson(request) ); 363 } 364 } 365 Object[] parameters = getIndividualRequestParameters( request); 366 String action = request.getAction(); 367 String method = request.getMethod(); 368 Object result = dispatch(action, method, parameters); 369 JsonSuccessResponse response = new JsonSuccessResponse( request.getTid(), action, method, result); 370 if( isBatched ) { 371 if( logger.isDebugEnabled() ) { 372 timer.stop(); 373 timer.logDebugTimeInMilliseconds( " - Individual request #" + requestNumber + " response data=>" + getGson().toJson(response) ); 374 resultReported = true; 375 } 376 } 377 return response; 378 } 379 catch( Throwable t ) { 380 JsonErrorResponse response = createJsonServerErrorResponse(request, t); 381 logger.error( "(Controlled) server error: " + t.getMessage() + " for Method '" + request.getAction() + "." + request.getMethod() + "'", t); 382 return response; 383 } 384 finally { 385 if( !resultReported ) { 386 timer.stop(); 387 // No point in logging individual requests when the request is not batched 388 if( isBatched ) { 389 if( logger.isDebugEnabled() ) { 390 timer.logDebugTimeInMilliseconds( " - Individual request #" + requestNumber + ": " + request.getAction() + "." + request.getMethod() + ". Time"); 391 } 392 } 393 } 394 } 395 } 396 397 private JsonObject[] parseIndividualJsonRequests(String requestString, JsonParser parser) { 398 assert !StringUtils.isEmpty(requestString); 399 assert parser != null; 400 401 JsonObject[] individualRequests; 402 JsonElement root = parser.parse( requestString ); 403 if( root.isJsonArray() ) { 404 JsonArray rootArray = (JsonArray)root; 405 if( rootArray.size() == 0 ) { 406 RequestException ex = RequestException.forRequestBatchMustHaveAtLeastOneRequest(); 407 logger.error( ex.getMessage(), ex ); 408 throw ex; 409 } 410 411 individualRequests = new JsonObject[rootArray.size()]; 412 int i = 0; 413 for( JsonElement item : rootArray ) { 414 if( !item.isJsonObject()) { 415 RequestException ex = RequestException.forRequestBatchItemMustBeAValidJsonObject(i); 416 logger.error( ex.getMessage(), ex ); 417 throw ex; 418 } 419 individualRequests[i] = (JsonObject)item; 420 i++; 421 } 422 } 423 else if( root.isJsonObject() ) { 424 individualRequests = new JsonObject[] {(JsonObject)root}; 425 } 426 else { 427 RequestException ex = RequestException.forRequestMustBeAValidJsonObjectOrArray(); 428 logger.error( ex.getMessage(), ex ); 429 throw ex; 430 } 431 432 return individualRequests; 433 } 434 435 private JsonRequest createIndividualJsonRequest( JsonObject element ) { 436 assert element != null; 437 438 String action = getNonEmptyJsonString( element, JsonRequest.ACTION_ELEMENT ); 439 String method = getNonEmptyJsonString( element, JsonRequest.METHOD_ELEMENT ); 440 Long tid = getNonEmptyJsonLong( element, JsonRequest.TID_ELEMENT ); 441 String type = getNonEmptyJsonString( element, JsonRequest.TYPE_ELEMENT ); 442 String jsonData = getMethodParametersJsonString(element); 443 JsonRequest result = new JsonRequest( type, action, method, tid, jsonData ); 444 return result; 445 } 446 447 private String getMethodParametersJsonString(JsonObject object) { 448 assert object != null; 449 450 JsonElement data = object.get(JsonRequest.DATA_ELEMENT); 451 if( data == null ) { 452 RequestException ex = RequestException.forJsonElementMissing(JsonRequest.DATA_ELEMENT); 453 logger.error( ex.getMessage(), ex ); 454 throw ex; 455 } 456 457 if( !data.isJsonNull() && !data.isJsonArray()) { 458 RequestException ex = RequestException.forJsonElementMustBeAJsonArray(JsonRequest.DATA_ELEMENT, data.toString()); 459 logger.error( ex.getMessage(), ex ); 460 throw ex; 461 } 462 463 return data.toString(); 464 } 465 466 private <T> T getNonEmptyJsonPrimitiveValue( JsonObject object, String elementName, PrimitiveJsonValueGetter<T> getter ) { 467 assert object != null; 468 assert !StringUtils.isEmpty(elementName); 469 470 try { 471 JsonElement element = object.get( elementName ); 472 if( element == null ) { 473 RequestException ex = RequestException.forJsonElementMissing(elementName); 474 logger.error( ex.getMessage(), ex ); 475 throw ex; 476 } 477 478 // Take into account that the element must be a primitive, and then a string! 479 T result = null; 480 if( element.isJsonPrimitive() ) { 481 result = getter.checkedGet( (JsonPrimitive) element ); 482 } 483 484 if( result == null ) { 485 RequestException ex = RequestException.forJsonElementMustBeANonNullOrEmptyValue(elementName, getter.getValueType() ); 486 logger.error( ex.getMessage(), ex ); 487 throw ex; 488 } 489 490 return result; 491 } 492 catch( JsonParseException e ) { 493 String message = "Probably a DirectJNgine BUG: there should not be JSON parse exceptions: we should have check ALL error conditions. " + e.getMessage(); 494 logger.error( message, e ); 495 assert false : message; 496 throw e; // Just to make the compiler happy -because of the assert 497 } 498 } 499 500 // A simple interface that helps us avoid duplicated code 501 // Its purpose is to retrieve a primitive value, or null 502 // if the primitive value is null or "empty" (makes sense for strings...) 503 interface PrimitiveJsonValueGetter<T> { 504 // Must return null if the specified primitive is not of type T or is "empty" 505 T checkedGet( JsonPrimitive value ); 506 Class<T> getValueType(); 507 } 508 509 510 private Long getNonEmptyJsonLong( JsonObject object, String elementName ) { 511 assert object != null; 512 assert !StringUtils.isEmpty(elementName); 513 514 return getNonEmptyJsonPrimitiveValue( object, elementName, new PrimitiveJsonValueGetter<Long>() { 515 // @Override 516 public Long checkedGet(JsonPrimitive value) { 517 assert value != null; 518 519 if( value.isNumber() ) { 520 String v = value.toString(); 521 try { 522 return new Long( Long.parseLong(v) ); 523 } 524 catch( NumberFormatException ex ) { 525 return null; 526 } 527 } 528 return null; 529 } 530 531 // @Override 532 public Class<Long> getValueType() { 533 return Long.class; 534 } 535 }); 536 } 537 538 private String getNonEmptyJsonString( JsonObject object, String elementName ) { 539 assert object != null; 540 assert !StringUtils.isEmpty(elementName); 541 542 return getNonEmptyJsonPrimitiveValue( object, elementName, new PrimitiveJsonValueGetter<String>() { 543 // @Override 544 public String checkedGet(JsonPrimitive value) { 545 assert value != null; 546 547 if( value.isString() ) { 548 String result = value.getAsString(); 549 if( result.equals("") ) 550 result = null; 551 return result; 552 } 553 return null; 554 } 555 556 // @Override 557 public Class<String> getValueType() { 558 return String.class; 559 } 560 561 }); 562 } 563 564 }