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