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