001/**
002 * Copyright (c) 2010-2019 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb;
023
024import static com.restfb.logging.RestFBLogger.MAPPER_LOGGER;
025import static com.restfb.util.ObjectUtil.isEmptyCollectionOrMap;
026import static com.restfb.util.ReflectionUtils.*;
027import static com.restfb.util.StringUtils.isBlank;
028import static com.restfb.util.StringUtils.trimToEmpty;
029import static java.lang.String.format;
030import static java.util.Collections.unmodifiableList;
031import static java.util.Collections.unmodifiableSet;
032
033import java.lang.reflect.Field;
034import java.lang.reflect.InvocationTargetException;
035import java.lang.reflect.Method;
036import java.math.BigDecimal;
037import java.math.BigInteger;
038import java.util.*;
039import java.util.Map.Entry;
040
041import com.restfb.exception.FacebookJsonMappingException;
042import com.restfb.json.*;
043import com.restfb.types.Comments;
044import com.restfb.util.DateUtils;
045import com.restfb.util.StringJsonUtils;
046import com.restfb.util.ReflectionUtils.*;
047
048/**
049 * Default implementation of a JSON-to-Java mapper.
050 * 
051 * @author <a href="http://restfb.com">Mark Allen</a>
052 */
053public class DefaultJsonMapper implements JsonMapper {
054
055  /**
056   * Helper to convert {@link JsonValue} into a given type
057   */
058  private final JsonHelper jsonHelper;
059
060  /**
061   * Creates a JSON mapper which will throw {@link com.restfb.exception.FacebookJsonMappingException} whenever an error
062   * occurs when mapping JSON data to Java objects.
063   */
064  public DefaultJsonMapper() {
065    jsonHelper = new JsonHelper();
066  }
067
068  @Override
069  public <T> List<T> toJavaList(String json, Class<T> type) {
070    if (type == null) {
071      throw new FacebookJsonMappingException("You must specify the Java type to map to.");
072    }
073
074    json = trimToEmpty(json);
075
076    if (isBlank(json)) {
077      throw new FacebookJsonMappingException("JSON is an empty string - can't map it.");
078    }
079
080    if (StringJsonUtils.isObject(json)) {
081      // Sometimes Facebook returns the empty object {} when it really should be
082      // returning an empty list [] (example: do an FQL query for a user's
083      // affiliations - it's a list except when there are none, then it turns
084      // into an object). Check for that special case here.
085      if (StringJsonUtils.isEmptyObject(json)) {
086        MAPPER_LOGGER
087          .trace("Encountered \\{} when we should've seen []. Mapping the \\{} as an empty list and moving on...");
088
089        return new ArrayList<>();
090      }
091
092      // Special case: if the only element of this object is an array called
093      // "data", then treat it as a list. The Graph API uses this convention for
094      // connections and in a few other places, e.g. comments on the Post
095      // object.
096      // Doing this simplifies mapping, so we don't have to worry about having a
097      // little placeholder object that only has a "data" value.
098      try {
099        JsonObject jsonObject = Json.parse(json).asObject();
100        List<String> fieldNames = jsonObject.names();
101
102        if (!fieldNames.isEmpty()) {
103          boolean hasSingleDataProperty = fieldNames.size() == 1;
104          Object jsonDataObject = jsonObject.get(fieldNames.get(0));
105
106          if (!hasSingleDataProperty && !(jsonDataObject instanceof JsonArray)) {
107            throw new FacebookJsonMappingException(
108              "JSON is an object but is being mapped as a list instead. Offending JSON is '" + json + "'.");
109          }
110
111          json = jsonDataObject.toString();
112        }
113      } catch (ParseException e) {
114        // Should never get here, but just in case...
115        throw new FacebookJsonMappingException("Unable to convert Facebook response JSON to a list of " + type.getName()
116            + " instances.  Offending JSON is '" + json + "'.",
117          e);
118      }
119    }
120
121    try {
122      JsonArray jsonArray = Json.parse(json).asArray();
123      List<T> list = new ArrayList<>(jsonArray.size());
124      for (JsonValue jsonValue: jsonArray) {
125        String innerJson = jsonHelper.getStringFrom(jsonValue);
126        // the inner JSON starts with square brackets but the parser don't think this is a JSON array
127        // so we think the parser is right and add quotes around the string
128        // solves Issue #719
129        if (jsonValue.isString() && innerJson.startsWith("[")) {
130          innerJson = '"' + innerJson + '"';
131        }
132        list.add(toJavaObject(innerJson, type));
133      }
134      return unmodifiableList(list);
135    } catch (FacebookJsonMappingException e) {
136      throw e;
137    } catch (Exception e) {
138      throw new FacebookJsonMappingException(
139        "Unable to convert Facebook response JSON to a list of " + type.getName() + " instances", e);
140    }
141  }
142
143  @Override
144  @SuppressWarnings("unchecked")
145  public <T> T toJavaObject(String json, Class<T> type) {
146    if (StringJsonUtils.isEmptyList(json)) {
147      return toJavaObject(StringJsonUtils.EMPTY_OBJECT, type);
148    }
149
150    basicJsonStringChecks(json);
151
152    try {
153      // Are we asked to map to JsonObject? If so, short-circuit right away.
154      if (type.equals(JsonObject.class)) {
155        return (T) Json.parse(json).asObject();
156      }
157
158      List<FieldWithAnnotation<Facebook>> fieldsWithAnnotation = findFieldsWithAnnotation(type, Facebook.class);
159      Set<String> facebookFieldNamesWithMultipleMappings = facebookFieldNamesWithMultipleMappings(fieldsWithAnnotation);
160
161      // If there are no annotated fields, assume we're mapping to a built-in
162      // type. If this is actually the empty object, just return a new instance
163      // of the corresponding Java type.
164      if (fieldsWithAnnotation.isEmpty()) {
165        if (StringJsonUtils.isEmptyObject(json)) {
166          T instance = createInstance(type);
167
168          // If there are any methods annotated with @JsonMappingCompleted,
169          // invoke them.
170          invokeJsonMappingCompletedMethods(instance);
171
172          return instance;
173        } else {
174          return toPrimitiveJavaType(json, type);
175        }
176      }
177
178      // Facebook will sometimes return the string "null".
179      // Check for that and bail early if we find it.
180      if (StringJsonUtils.isNull(json)) {
181        return null;
182      }
183
184      // Facebook will sometimes return the string "false" to mean null.
185      // Check for that and bail early if we find it.
186      if (StringJsonUtils.isFalse(json)) {
187        MAPPER_LOGGER.debug("Encountered 'false' from Facebook when trying to map to {} - mapping null instead.",
188          type.getSimpleName());
189        return null;
190      }
191
192      JsonValue jsonValue = Json.parse(json);
193      T instance = createInstance(type);
194
195      if (instance instanceof JsonObject) {
196        return (T) jsonValue.asObject();
197      }
198
199      if (!jsonValue.isObject()) {
200        return null;
201      }
202
203      JsonObject jsonObject = jsonValue.asObject();
204
205      // For each Facebook-annotated field on the current Java object, pull data
206      // out of the JSON object and put it in the Java object
207      for (FieldWithAnnotation<Facebook> fieldWithAnnotation : fieldsWithAnnotation) {
208        String facebookFieldName = getFacebookFieldName(fieldWithAnnotation);
209
210        if (jsonObject.get(facebookFieldName) == null) {
211          MAPPER_LOGGER.trace("No JSON value present for '{}', skipping. JSON is '{}'.", facebookFieldName, json);
212          continue;
213        }
214
215        fieldWithAnnotation.getField().setAccessible(true);
216
217        // Set the Java field's value.
218        //
219        // If we notice that this Facebook field name is mapped more than once,
220        // go into a special mode where we swallow any exceptions that occur
221        // when mapping to the Java field. This is because Facebook will
222        // sometimes return data in different formats for the same field name.
223        // See issues 56 and 90 for examples of this behavior and discussion.
224        if (facebookFieldNamesWithMultipleMappings.contains(facebookFieldName)) {
225          try {
226            fieldWithAnnotation.getField().set(instance,
227              toJavaType(fieldWithAnnotation, jsonObject, facebookFieldName));
228          } catch (FacebookJsonMappingException | ParseException | UnsupportedOperationException e) {
229            logMultipleMappingFailedForField(facebookFieldName, fieldWithAnnotation, json);
230          }
231        } else {
232          fieldWithAnnotation.getField().set(instance, toJavaType(fieldWithAnnotation, jsonObject, facebookFieldName));
233        }
234      }
235
236      // If there are any methods annotated with @JsonMappingCompleted,
237      // invoke them.
238      invokeJsonMappingCompletedMethods(instance);
239
240      return instance;
241    } catch (FacebookJsonMappingException e) {
242      throw e;
243    } catch (Exception e) {
244      throw new FacebookJsonMappingException("Unable to map JSON to Java. Offending JSON is '" + json + "'.", e);
245    }
246  }
247
248  private void basicJsonStringChecks(String json) {
249    if (isBlank(json)) {
250      throw new FacebookJsonMappingException("JSON is an empty string - can't map it.");
251    }
252
253    if (StringJsonUtils.isList(json)) {
254      throw new FacebookJsonMappingException("JSON is an array but is being mapped as an object "
255          + "- you should map it as a List instead. Offending JSON is '" + json + "'.");
256    }
257  }
258
259  /**
260   * Finds and invokes methods on {@code object} that are annotated with the {@code @JsonMappingCompleted} annotation.
261   * <p>
262   * This will even work on {@code private} methods.
263   * 
264   * @param object
265   *          The object on which to invoke the method.
266   * @throws IllegalAccessException
267   *           If unable to invoke the method.
268   * @throws InvocationTargetException
269   *           If unable to invoke the method.
270   */
271  protected void invokeJsonMappingCompletedMethods(Object object)
272      throws IllegalAccessException, InvocationTargetException {
273    for (Method method : findMethodsWithAnnotation(object.getClass(), JsonMappingCompleted.class)) {
274      method.setAccessible(true);
275
276      if (method.getParameterTypes().length == 0)
277        method.invoke(object);
278      else if (method.getParameterTypes().length == 1 && JsonMapper.class.equals(method.getParameterTypes()[0]))
279        method.invoke(object, this);
280      else
281        throw new FacebookJsonMappingException(
282          format("Methods annotated with @%s must take 0 parameters or a single %s parameter. Your method was %s",
283            JsonMappingCompleted.class.getSimpleName(), JsonMapper.class.getSimpleName(), method));
284    }
285  }
286
287  /**
288   * Dumps out a log message when one of a multiple-mapped Facebook field name JSON-to-Java mapping operation fails.
289   * 
290   * @param facebookFieldName
291   *          The Facebook field name.
292   * @param fieldWithAnnotation
293   *          The Java field to map to and its annotation.
294   * @param json
295   *          The JSON that failed to map to the Java field.
296   */
297  protected void logMultipleMappingFailedForField(String facebookFieldName,
298      FieldWithAnnotation<Facebook> fieldWithAnnotation, String json) {
299    if (!MAPPER_LOGGER.isTraceEnabled()) {
300      return;
301    }
302
303    Field field = fieldWithAnnotation.getField();
304
305    MAPPER_LOGGER.trace(
306      "Could not map '{}' to {}. {}, but continuing on because '{}"
307          + "' is mapped to multiple fields in {}. JSON is {}",
308      facebookFieldName, field.getDeclaringClass().getSimpleName(), field.getName(), facebookFieldName,
309      field.getDeclaringClass().getSimpleName(), json);
310  }
311
312  /**
313   * For a Java field annotated with the {@code Facebook} annotation, figure out what the corresponding Facebook JSON
314   * field name to map to it is.
315   * 
316   * @param fieldWithAnnotation
317   *          A Java field annotated with the {@code Facebook} annotation.
318   * @return The Facebook JSON field name that should be mapped to this Java field.
319   */
320  protected String getFacebookFieldName(FieldWithAnnotation<Facebook> fieldWithAnnotation) {
321    String facebookFieldName = fieldWithAnnotation.getAnnotation().value();
322    Field field = fieldWithAnnotation.getField();
323
324    // If no Facebook field name was specified in the annotation, assume
325    // it's the same name as the Java field
326    if (isBlank(facebookFieldName)) {
327      MAPPER_LOGGER.trace("No explicit Facebook field name found for {}, so defaulting to the field name itself ({})",
328        field, field.getName());
329
330      facebookFieldName = field.getName();
331    }
332
333    return facebookFieldName;
334  }
335
336  /**
337   * Finds any Facebook JSON fields that are mapped to more than 1 Java field.
338   * 
339   * @param fieldsWithAnnotation
340   *          Java fields annotated with the {@code Facebook} annotation.
341   * @return Any Facebook JSON fields that are mapped to more than 1 Java field.
342   */
343  protected Set<String> facebookFieldNamesWithMultipleMappings(
344      List<FieldWithAnnotation<Facebook>> fieldsWithAnnotation) {
345    Map<String, Integer> facebookFieldsNamesWithOccurrenceCount = new HashMap<>();
346    Set<String> facebookFieldNamesWithMultipleMappings = new HashSet<>();
347
348    // Get a count of Facebook field name occurrences for each
349    // @Facebook-annotated field
350    for (FieldWithAnnotation<Facebook> fieldWithAnnotation : fieldsWithAnnotation) {
351      String fieldName = getFacebookFieldName(fieldWithAnnotation);
352      int occurrenceCount = facebookFieldsNamesWithOccurrenceCount.containsKey(fieldName)
353          ? facebookFieldsNamesWithOccurrenceCount.get(fieldName)
354          : 0;
355      facebookFieldsNamesWithOccurrenceCount.put(fieldName, occurrenceCount + 1);
356    }
357
358    // Pull out only those field names with multiple mappings
359    for (Entry<String, Integer> entry : facebookFieldsNamesWithOccurrenceCount.entrySet()) {
360      if (entry.getValue() > 1) {
361        facebookFieldNamesWithMultipleMappings.add(entry.getKey());
362      }
363    }
364
365    return unmodifiableSet(facebookFieldNamesWithMultipleMappings);
366  }
367
368  @Override
369  public String toJson(Object object) {
370    return toJson(object, false);
371  }
372
373  @Override
374  public String toJson(Object object, boolean ignoreNullValuedProperties) {
375    JsonValue jsonObj = toJsonInternal(object, ignoreNullValuedProperties);
376    return jsonHelper.getStringFrom(jsonObj);
377  }
378
379  /**
380   * Recursively marshal the given {@code object} to JSON.
381   * <p>
382   * Used by {@link #toJson(Object)}.
383   * 
384   * @param object
385   *          The object to marshal.
386   * @param ignoreNullValuedProperties
387   *          If this is {@code true}, no Javabean properties with {@code null} values will be included in the generated
388   *          JSON.
389   * @return JSON representation of the given {@code object}.
390   * @throws FacebookJsonMappingException
391   *           If an error occurs while marshaling to JSON.
392   */
393  protected JsonValue toJsonInternal(Object object, boolean ignoreNullValuedProperties) {
394    if (object == null) {
395      return Json.NULL;
396    }
397
398    if (object instanceof JsonValue) {
399      return (JsonValue) object;
400    }
401
402    if (object instanceof List<?>) {
403      JsonArray jsonArray = new JsonArray();
404      for (Object o : (List<?>) object)
405        jsonArray.add(toJsonInternal(o, ignoreNullValuedProperties));
406
407      return jsonArray;
408    }
409
410    if (object instanceof Map<?, ?>) {
411      JsonObject jsonObject = new JsonObject();
412      for (Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
413        if (!(entry.getKey() instanceof String)) {
414          throw new FacebookJsonMappingException("Your Map keys must be of type " + String.class
415              + " in order to be converted to JSON.  Offending map is " + object);
416        }
417
418        try {
419          jsonObject.add((String) entry.getKey(), toJsonInternal(entry.getValue(), ignoreNullValuedProperties));
420        } catch (ParseException | IllegalArgumentException e) {
421          throw new FacebookJsonMappingException(
422            "Unable to process value '" + entry.getValue() + "' for key '" + entry.getKey() + "' in Map " + object, e);
423        }
424      }
425
426      return jsonObject;
427    }
428
429    if (isPrimitive(object)) {
430      return primitiveToJsonValue(object);
431    }
432
433    if (object instanceof BigInteger) {
434      return Json.value(((BigInteger) object).longValue());
435    }
436
437    if (object instanceof BigDecimal) {
438      return Json.value(((BigDecimal) object).doubleValue());
439    }
440
441    if (object instanceof Enum) {
442      return Json.value(((Enum) object).name());
443    }
444
445    if (object instanceof Date) {
446      return Json.value(DateUtils.toLongFormatFromDate((Date) object));
447    }
448
449    // We've passed the special-case bits, so let's try to marshal this as a
450    // plain old Javabean...
451
452    List<FieldWithAnnotation<Facebook>> fieldsWithAnnotation =
453        findFieldsWithAnnotation(object.getClass(), Facebook.class);
454
455    JsonObject jsonObject = new JsonObject();
456
457    // No longer throw an exception in this case. If there are multiple fields
458    // with the same @Facebook value, it's luck of the draw which is picked for
459    // JSON marshaling.
460    // TODO: A better implementation would query each duplicate-mapped field. If
461    // it has is a non-null value and the other duplicate values are null, use
462    // the non-null field.
463    Set<String> facebookFieldNamesWithMultipleMappings = facebookFieldNamesWithMultipleMappings(fieldsWithAnnotation);
464    if (!facebookFieldNamesWithMultipleMappings.isEmpty() && MAPPER_LOGGER.isDebugEnabled()) {
465      MAPPER_LOGGER.debug(
466        "Unable to convert to JSON because multiple @{} annotations for the same name are present: {}",
467        Facebook.class.getSimpleName(), facebookFieldNamesWithMultipleMappings);
468    }
469
470    for (FieldWithAnnotation<Facebook> fieldWithAnnotation : fieldsWithAnnotation) {
471      String facebookFieldName = getFacebookFieldName(fieldWithAnnotation);
472      fieldWithAnnotation.getField().setAccessible(true);
473
474      try {
475        Object fieldValue = fieldWithAnnotation.getField().get(object);
476
477        if (!(ignoreNullValuedProperties && (fieldValue == null || isEmptyCollectionOrMap(fieldValue)))) {
478          jsonObject.add(facebookFieldName, toJsonInternal(fieldValue, ignoreNullValuedProperties));
479        }
480      } catch (Exception e) {
481        throw new FacebookJsonMappingException(
482          "Unable to process field '" + facebookFieldName + "' for " + object.getClass(), e);
483      }
484    }
485
486    return jsonObject;
487  }
488
489  /**
490   * Given a {@code json} value of something like {@code MyValue} or {@code 123} , return a representation of that value
491   * of type {@code type}.
492   * <p>
493   * This is to support non-legal JSON served up by Facebook for API calls like {@code Friends.get} (example result:
494   * {@code [222333,1240079]}).
495   * 
496   * @param <T>
497   *          The Java type to map to.
498   * @param json
499   *          The non-legal JSON to map to the Java type.
500   * @param type
501   *          Type token.
502   * @return Java representation of {@code json}.
503   * @throws FacebookJsonMappingException
504   *           If an error occurs while mapping JSON to Java.
505   */
506  @SuppressWarnings("unchecked")
507  protected <T> T toPrimitiveJavaType(String json, Class<T> type) {
508
509    // cleanup the json string
510    if (json.length() > 1 && json.startsWith("\"") && json.endsWith("\"")) {
511      json = json.replaceFirst("\"", "");
512      json = json.substring(0, json.length() - 1);
513    }
514
515    if (String.class.equals(type)) {
516      return (T) json;
517    }
518    if (Integer.class.equals(type) || Integer.TYPE.equals(type)) {
519      return (T) Integer.valueOf(json);
520    }
521    if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
522      return (T) Boolean.valueOf(json);
523    }
524    if (Long.class.equals(type) || Long.TYPE.equals(type)) {
525      return (T) Long.valueOf(json);
526    }
527    if (Double.class.equals(type) || Double.TYPE.equals(type)) {
528      return (T) Double.valueOf(json);
529    }
530    if (Float.class.equals(type) || Float.TYPE.equals(type)) {
531      return (T) Float.valueOf(json);
532    }
533    if (BigInteger.class.equals(type)) {
534      return (T) new BigInteger(json);
535    }
536    if (BigDecimal.class.equals(type)) {
537      return (T) new BigDecimal(json);
538    }
539
540    throw new FacebookJsonMappingException("Don't know how to map JSON to " + type
541        + ". Are you sure you're mapping to the right class?\nOffending JSON is '" + json + "'.");
542  }
543
544  /**
545   * Extracts JSON data for a field according to its {@code Facebook} annotation and returns it converted to the proper
546   * Java type.
547   * 
548   * @param fieldWithAnnotation
549   *          The field/annotation pair which specifies what Java type to convert to.
550   * @param jsonObject
551   *          "Raw" JSON object to pull data from.
552   * @param facebookFieldName
553   *          Specifies what JSON field to pull "raw" data from.
554   * @return A
555   * @throws ParseException
556   *           If an error occurs while mapping JSON to Java.
557   * @throws FacebookJsonMappingException
558   *           If an error occurs while mapping JSON to Java.
559   */
560  protected Object toJavaType(FieldWithAnnotation<Facebook> fieldWithAnnotation, JsonObject jsonObject,
561      String facebookFieldName) {
562    Class<?> type = fieldWithAnnotation.getField().getType();
563    JsonValue rawValue = jsonObject.get(facebookFieldName);
564
565    // Short-circuit right off the bat if we've got a null value.
566    if (rawValue.isNull()) {
567      return null;
568    }
569
570    if (String.class.equals(type)) {
571      /*
572       * Special handling here for better error checking.
573       *
574       * Since {@code JsonObject.getString()} will return literal JSON text even if it's _not_ a JSON string, we check
575       * the marshaled type and bail if needed. For example, calling {@code JsonObject.getString("results")} on the
576       * below JSON...
577       *
578       * <code> { "results":[ {"name":"Mark Allen"} ] } </code>
579       *
580       * ... would return the string {@code "[{"name":"Mark Allen"}]"} instead of throwing an error. So we throw the
581       * error ourselves.
582       *
583       * Per Antonello Naccarato, sometimes FB will return an empty JSON array instead of an empty string. Look for that
584       * here.
585       */
586      if (rawValue.isArray() && rawValue.asArray().isEmpty()) {
587        MAPPER_LOGGER.trace("Coercing an empty JSON array to an empty string for {}", fieldWithAnnotation);
588
589        return "";
590      }
591
592      /*
593       * If the user wants a string, _always_ give her a string.
594       *
595       * This is useful if, for example, you've got a @Facebook-annotated string field that you'd like to have a numeric
596       * type shoved into.
597       *
598       * User beware: this will turn *anything* into a string, which might lead to results you don't expect.
599       */
600      return jsonHelper.getStringFrom(rawValue);
601    }
602
603    if (Integer.class.equals(type) || Integer.TYPE.equals(type)) {
604      return jsonHelper.getIntegerFrom(rawValue);
605    }
606    if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) {
607      return jsonHelper.getBooleanFrom(rawValue);
608    }
609    if (Long.class.equals(type) || Long.TYPE.equals(type)) {
610      return jsonHelper.getLongFrom(rawValue);
611    }
612    if (Double.class.equals(type) || Double.TYPE.equals(type)) {
613      return jsonHelper.getDoubleFrom(rawValue);
614    }
615    if (Float.class.equals(type) || Float.TYPE.equals(type)) {
616      return jsonHelper.getFloatFrom(rawValue);
617    }
618    if (BigInteger.class.equals(type)) {
619      return jsonHelper.getBigIntegerFrom(rawValue);
620    }
621    if (BigDecimal.class.equals(type)) {
622      return jsonHelper.getBigDecimalFrom(rawValue);
623    }
624    if (List.class.equals(type)) {
625      return toJavaList(rawValue.toString(), getFirstParameterizedTypeArgument(fieldWithAnnotation.getField()));
626    }
627    if (Map.class.equals(type)) {
628      return convertJsonObjectToMap(rawValue.toString(), fieldWithAnnotation.getField());
629    }
630
631    if (type.isEnum()) {
632      Class<? extends Enum> enumType = type.asSubclass(Enum.class);
633      try {
634        return Enum.valueOf(enumType, rawValue.asString());
635      } catch (IllegalArgumentException iae) {
636        MAPPER_LOGGER.debug("Cannot map string {} to enum {}, try fallback toUpperString next...", rawValue.asString(),
637          enumType.getName());
638      }
639      try {
640        return Enum.valueOf(enumType, rawValue.asString().toUpperCase());
641      } catch (IllegalArgumentException iae) {
642        MAPPER_LOGGER.debug("Mapping string {} to enum {} not possible", rawValue.asString(), enumType.getName());
643      }
644    }
645
646    if (Date.class.equals(type)) {
647      return DateUtils.toDateFromLongFormat(jsonHelper.getStringFrom(rawValue));
648    }
649
650    String rawValueAsString = jsonHelper.getStringFrom(rawValue);
651
652    // Hack for issue #76 where FB will sometimes return a Post's Comments as
653    // "[]" instead of an object type (wtf)F
654    if (Comments.class.isAssignableFrom(type) && rawValue instanceof JsonArray) {
655      MAPPER_LOGGER.debug(
656        "Encountered comment array '{}' but expected a {} object instead.  Working around that by coercing "
657            + "into an empty {} instance...",
658        rawValueAsString, Comments.class.getSimpleName(), Comments.class.getSimpleName());
659
660      JsonObject workaroundJsonObject = new JsonObject();
661      workaroundJsonObject.add("total_count", 0);
662      workaroundJsonObject.add("data", new JsonArray());
663      rawValueAsString = workaroundJsonObject.toString();
664    }
665
666    // Some other type - recurse into it
667    return toJavaObject(rawValueAsString, type);
668  }
669
670  private Map convertJsonObjectToMap(String json, Field field) {
671    Class<?> firstParam = getFirstParameterizedTypeArgument(field);
672    if (!String.class.equals(firstParam)) {
673      throw new FacebookJsonMappingException("The java type map needs to have a 'String' key, but is " + firstParam);
674    }
675
676    Class<?> secondParam = getSecondParameterizedTypeArgument(field);
677
678    if (json.startsWith("{")) {
679      JsonObject jsonObject = Json.parse(json).asObject();
680      Map<String, Object> map = new HashMap();
681      for (String key : jsonObject.names()) {
682        String value = jsonHelper.getStringFrom(jsonObject.get(key));
683        map.put(key, toJavaObject(value, secondParam));
684      }
685      return map;
686    }
687
688    return null;
689  }
690
691  private JsonValue primitiveToJsonValue(Object object) {
692    if (object == null) {
693      return Json.NULL;
694    }
695
696    Class<?> type = object.getClass();
697
698    if (object instanceof String) {
699      return Json.value((String) object);
700    }
701
702    if (object instanceof Integer || Integer.TYPE.equals(type)) {
703      return Json.value((Integer) object);
704    }
705
706    if (object instanceof Boolean || Boolean.TYPE.equals(type)) {
707      return Json.value((Boolean) object);
708    }
709
710    if (object instanceof Long || Long.TYPE.equals(type)) {
711      return Json.value((Long) object);
712    }
713
714    if (object instanceof Double || Double.TYPE.equals(type)) {
715      return Json.value((Double) object);
716    }
717
718    if (object instanceof Float || Float.TYPE.equals(type)) {
719      return Json.value((Float) object);
720    }
721
722    if (object instanceof Byte || Byte.TYPE.equals(type)) {
723      return Json.value((Byte) object);
724    }
725    if (object instanceof Short || Short.TYPE.equals(type)) {
726      return Json.value((Short) object);
727    }
728
729    if (object instanceof Character || Character.TYPE.equals(type)) {
730      return Json.value(Character.toString((Character) object));
731    }
732
733    return Json.NULL;
734
735  }
736}