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    }