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    }