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