001/*
002 * Copyright (c) 2010-2026 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.lang.reflect.ParameterizedType;
037import java.lang.reflect.Type;
038import java.math.BigDecimal;
039import java.math.BigInteger;
040import java.util.*;
041import java.util.Map.Entry;
042import java.util.stream.Collectors;
043
044import com.restfb.exception.FacebookJsonMappingException;
045import com.restfb.json.*;
046import com.restfb.types.AbstractFacebookType;
047import com.restfb.types.Comments;
048import com.restfb.util.DateUtils;
049import com.restfb.util.ObjectUtil;
050import com.restfb.util.ReflectionUtils;
051import com.restfb.util.StringJsonUtils;
052
053/**
054 * Default implementation of a JSON-to-Java mapper.
055 *
056 * @author <a href="http://restfb.com">Mark Allen</a>
057 */
058public class DefaultJsonMapper implements JsonMapper {
059
060  private FacebookClient facebookClient;
061
062  /**
063   * Helper to convert {@link JsonValue} into a given type
064   */
065  private final JsonHelper jsonHelper;
066
067  /**
068   * Creates a JSON mapper which will throw {@link com.restfb.exception.FacebookJsonMappingException} whenever an error
069   * occurs when mapping JSON data to Java objects.
070   */
071  public DefaultJsonMapper() {
072    jsonHelper = new JsonHelper();
073  }
074
075  @Override
076  public void setFacebookClient(FacebookClient facebookClient) {
077    this.facebookClient = facebookClient;
078  }
079
080  @Override
081  @SuppressWarnings("unchecked")
082  public <T> List<T> toJavaList(String json, Class<T> type) {
083    ObjectUtil.requireNotNull(type,
084      () -> new FacebookJsonMappingException("You must specify the Java type to map to."));
085    json = trimToEmpty(json);
086
087    checkJsonNotBlank(json);
088    JsonValue jsonValue;
089    try {
090      jsonValue = Json.parse(json);
091    } catch (ParseException e) {
092      throw new FacebookJsonMappingException("Unable to convert Facebook response JSON to a list of " + type.getName()
093          + " instances. Offending JSON is '" + json + "'.",
094        e);
095    }
096
097    return toJavaList(jsonValue, type, json);
098  }
099
100  private <T> List<T> toJavaList(JsonValue jsonValue, Class<T> type) {
101    return toJavaList(jsonValue, type, null);
102  }
103
104  @SuppressWarnings("unchecked")
105  private <T> List<T> toJavaList(JsonValue jsonValue, Class<T> type, String rawJson) {
106    String sourceJson = rawJson != null ? rawJson : (jsonValue == null ? "null" : jsonValue.toString());
107    if (jsonValue != null && jsonValue.isObject()) {
108      JsonObject jsonObject = jsonValue.asObject();
109      if (jsonObject.isEmpty()) {
110        MAPPER_LOGGER
111          .trace("Encountered \\{} when we should've seen []. Mapping the \\{} as an empty list and moving on...");
112        return new ArrayList<>();
113      }
114
115      List<String> fieldNames = jsonObject.names();
116      if (!fieldNames.isEmpty()) {
117        boolean hasSingleDataProperty = fieldNames.size() == 1;
118        JsonValue jsonDataObject = jsonObject.get(fieldNames.get(0));
119        checkObjectIsMappedAsList(sourceJson, hasSingleDataProperty, jsonDataObject);
120        return toJavaList(jsonDataObject, type, jsonDataObject.toString());
121      }
122    }
123
124    try {
125      JsonArray jsonArray = jsonValue.asArray();
126      List<T> list = new ArrayList<>(jsonArray.size());
127      int addedCount = 0;
128      for (JsonValue element : jsonArray) {
129        if (element.isArray() && typeIsList(type)) {
130          @SuppressWarnings("unchecked")
131          T innerList = (T) convertRawValueToList(element, type);
132          list.add(innerList);
133          if (innerList != null || element.isNull()) {
134            addedCount++;
135          }
136        } else {
137          T converted = toJavaObject(element, type);
138          list.add(converted);
139          if (converted != null || element.isNull()) {
140            addedCount++;
141          }
142        }
143      }
144      if (addedCount == 0 && !jsonArray.isEmpty() && typeHasFacebookFields(type)) {
145        throw new FacebookJsonMappingException(
146          "Unable to convert Facebook response JSON to a list of " + type.getName() + " instances", null);
147      }
148      return unmodifiableList(list);
149    } catch (FacebookJsonMappingException e) {
150      throw e;
151    } catch (Exception e) {
152      throw new FacebookJsonMappingException("Unable to convert Facebook response JSON to a list of " + type.getName()
153          + " instances. Offending JSON is '" + sourceJson + "'.",
154        e);
155    }
156  }
157
158  private void checkObjectIsMappedAsList(String json, boolean hasSingleDataProperty, JsonValue jsonDataObject) {
159    if (!hasSingleDataProperty && !jsonDataObject.isArray()) {
160      throw new FacebookJsonMappingException(
161        "JSON is an object but is being mapped as a list instead. Offending JSON is '" + json + "'.");
162    }
163  }
164
165  private boolean typeHasFacebookFields(Class<?> type) {
166    return !findFieldsWithAnnotation(type, Facebook.class).isEmpty();
167  }
168
169  @Override
170  @SuppressWarnings("unchecked")
171  public <T> T toJavaObject(String json, Class<T> type) {
172    if (StringJsonUtils.isEmptyList(json)) {
173      json = StringJsonUtils.EMPTY_OBJECT;
174    }
175
176    checkJsonNotBlank(json);
177    checkJsonNotList(json);
178
179    if (JsonObject.class.equals(type)) {
180      try {
181        JsonValue parsed = Json.parse(json);
182        if (parsed.isString()) {
183          return type.cast(Json.parse(parsed.asString()).asObject());
184        }
185        return type.cast(parsed.asObject());
186      } catch (ParseException | UnsupportedOperationException e) {
187        throw new FacebookJsonMappingException(
188          "Unable to parse JSON into JsonObject. Offending JSON is '" + json + "'.", e);
189      }
190    }
191
192    List<FieldWithAnnotation<Facebook>> listOfFieldsWithAnnotation = findFieldsWithAnnotation(type, Facebook.class);
193    if (listOfFieldsWithAnnotation.isEmpty() && !StringJsonUtils.isEmptyObject(json)) {
194      return toPrimitiveJavaType(json, type);
195    }
196
197    try {
198      JsonValue jsonValue = Json.parse(json);
199      return toJavaObject(jsonValue, type, json);
200    } catch (FacebookJsonMappingException e) {
201      throw e;
202    } catch (Exception e) {
203      throw new FacebookJsonMappingException("Unable to map JSON to Java. Offending JSON is '" + json + "'.", e);
204    }
205  }
206
207  private <T> T toJavaObject(JsonValue jsonValue, Class<T> type) {
208    return toJavaObject(jsonValue, type, null);
209  }
210
211  private <T> T toJavaObject(JsonValue jsonValue, Class<T> type, String rawJson) {
212    String sourceJson = rawJson != null ? rawJson : (jsonValue == null ? "null" : jsonValue.toString());
213    try {
214      if (type.equals(JsonObject.class)) {
215        if (jsonValue.isString()) {
216          try {
217            return type.cast(Json.parse(jsonValue.asString()).asObject());
218          } catch (ParseException e) {
219            throw new FacebookJsonMappingException(
220              "Unable to parse JSON into JsonObject. Offending JSON is '" + jsonValue + "'.", e);
221          }
222        }
223        return type.cast(jsonValue.asObject());
224      }
225
226      List<FieldWithAnnotation<Facebook>> listOfFieldsWithAnnotation = findFieldsWithAnnotation(type, Facebook.class);
227      Set<String> facebookFieldNamesWithMultipleMappings =
228          facebookFieldNamesWithMultipleMappings(listOfFieldsWithAnnotation);
229
230      if (jsonValue == null || jsonValue.isNull()) {
231        return null;
232      }
233
234      if (jsonValue.isBoolean() && !jsonValue.asBoolean()) {
235        MAPPER_LOGGER.debug("Encountered 'false' from Facebook when trying to map to {} - mapping null instead.",
236          type.getSimpleName());
237        return null;
238      }
239
240      if (listOfFieldsWithAnnotation.isEmpty()) {
241        if (jsonValue.isObject() && jsonValue.asObject().isEmpty()) {
242          T instance = createInstance(type);
243          invokeJsonMappingCompletedMethods(instance);
244          return instance;
245        }
246        return toPrimitiveJavaType(sourceJson, type);
247      }
248
249      if (jsonValue.isArray()) {
250        JsonArray array = jsonValue.asArray();
251        if (array.isEmpty()) {
252          jsonValue = new JsonObject();
253        } else {
254          return null;
255        }
256      }
257
258      T instance = createInstance(type);
259
260      if (instance instanceof JsonObject) {
261        return type.cast(jsonValue.asObject());
262      }
263
264      if (!jsonValue.isObject()) {
265        return null;
266      }
267
268      JsonObject jsonObject = jsonValue.asObject();
269
270      handleAbstractFacebookType(sourceJson, instance);
271
272      for (FieldWithAnnotation<Facebook> fieldWithAnnotation : listOfFieldsWithAnnotation) {
273        String facebookFieldName = getFacebookFieldName(fieldWithAnnotation);
274
275        if (!jsonObject.contains(facebookFieldName)
276            && !fieldWithAnnotation.getField().getType().equals(Optional.class)) {
277          MAPPER_LOGGER.trace("No JSON value present for '{}', skipping. JSON is '{}'.", facebookFieldName, sourceJson);
278          continue;
279        }
280
281        fieldWithAnnotation.getField().setAccessible(true);
282
283        setJavaFileValue(sourceJson, facebookFieldNamesWithMultipleMappings, instance, jsonObject, fieldWithAnnotation,
284          facebookFieldName);
285      }
286
287      invokeJsonMappingCompletedMethods(instance);
288
289      return instance;
290    } catch (FacebookJsonMappingException e) {
291      throw e;
292    } catch (Exception e) {
293      throw new FacebookJsonMappingException("Unable to map JSON to Java. Offending JSON is '" + sourceJson + "'.", e);
294    }
295  }
296
297  private <T> void setJavaFileValue(String json, Set<String> facebookFieldNamesWithMultipleMappings, T instance,
298      JsonObject jsonObject, FieldWithAnnotation<Facebook> fieldWithAnnotation, String facebookFieldName)
299      throws IllegalAccessException {
300    // Set the Java field's value.
301    //
302    // If we notice that this Facebook field name is mapped more than once,
303    // go into a special mode where we swallow any exceptions that occur
304    // when mapping to the Java field. This is because Facebook will
305    // sometimes return data in different formats for the same field name.
306    // See issues 56 and 90 for examples of this behavior and discussion.
307    try {
308      fieldWithAnnotation.getField().set(instance, toJavaType(fieldWithAnnotation, jsonObject, facebookFieldName));
309    } catch (FacebookJsonMappingException | ParseException | UnsupportedOperationException e) {
310      if (facebookFieldNamesWithMultipleMappings.contains(facebookFieldName)) {
311        logMultipleMappingFailedForField(facebookFieldName, fieldWithAnnotation, json);
312      } else {
313        throw e;
314      }
315    }
316  }
317
318  private <T> void handleAbstractFacebookType(String json, T instance) {
319    if (instance instanceof AbstractFacebookType) {
320      ReflectionUtils.setJson(instance, json);
321    }
322  }
323
324  private void checkJsonNotBlank(String json) {
325    if (isBlank(json)) {
326      throw new FacebookJsonMappingException("JSON is an empty string - can't map it.");
327    }
328  }
329
330  private void checkJsonNotList(String json) {
331    if (StringJsonUtils.isList(json)) {
332      throw new FacebookJsonMappingException("JSON is an array but is being mapped as an object "
333          + "- you should map it as a List instead. Offending JSON is '" + json + "'.");
334    }
335  }
336
337  /**
338   * Finds and invokes methods on {@code object} that are annotated with the {@code @JsonMappingCompleted} annotation.
339   * <p>
340   * This will even work on {@code private} methods.
341   *
342   * @param object
343   *          The object on which to invoke the method.
344   * @throws IllegalAccessException
345   *           If unable to invoke the method.
346   * @throws InvocationTargetException
347   *           If unable to invoke the method.
348   */
349  protected void invokeJsonMappingCompletedMethods(Object object)
350      throws IllegalAccessException, InvocationTargetException {
351    for (Method method : findMethodsWithAnnotation(object.getClass(), JsonMappingCompleted.class)) {
352      method.setAccessible(true);
353
354      int methodParameterCount = method.getParameterTypes().length;
355
356      if (methodParameterCount == 0) {
357        method.invoke(object);
358      } else if (methodParameterCount == 1 && JsonMapper.class.equals(method.getParameterTypes()[0])) {
359        method.invoke(object, this);
360      } else {
361        throw new FacebookJsonMappingException(
362          format("Methods annotated with @%s must take 0 parameters or a single %s parameter. Your method was %s",
363            JsonMappingCompleted.class.getSimpleName(), JsonMapper.class.getSimpleName(), method));
364      }
365    }
366  }
367
368  /**
369   * Dumps out a log message when one of a multiple-mapped Facebook field name JSON-to-Java mapping operation fails.
370   *
371   * @param facebookFieldName
372   *          The Facebook field name.
373   * @param fieldWithAnnotation
374   *          The Java field to map to and its annotation.
375   * @param json
376   *          The JSON that failed to map to the Java field.
377   */
378  protected void logMultipleMappingFailedForField(String facebookFieldName,
379      FieldWithAnnotation<Facebook> fieldWithAnnotation, String json) {
380    if (!MAPPER_LOGGER.isTraceEnabled()) {
381      return;
382    }
383
384    Field field = fieldWithAnnotation.getField();
385
386    MAPPER_LOGGER.trace(
387      "Could not map '{}' to {}. {}, but continuing on because '{}"
388          + "' is mapped to multiple fields in {}. JSON is {}",
389      facebookFieldName, field.getDeclaringClass().getSimpleName(), field.getName(), facebookFieldName,
390      field.getDeclaringClass().getSimpleName(), json);
391  }
392
393  /**
394   * For a Java field annotated with the {@code Facebook} annotation, figure out what the corresponding Facebook JSON
395   * field name to map to it is.
396   *
397   * @param fieldWithAnnotation
398   *          A Java field annotated with the {@code Facebook} annotation.
399   * @return The Facebook JSON field name that should be mapped to this Java field.
400   */
401  protected String getFacebookFieldName(FieldWithAnnotation<Facebook> fieldWithAnnotation) {
402    String facebookFieldName = fieldWithAnnotation.getAnnotation().value();
403    Field field = fieldWithAnnotation.getField();
404
405    // If no Facebook field name was specified in the annotation, assume
406    // it's the same name as the Java field
407    if (isBlank(facebookFieldName)) {
408      MAPPER_LOGGER.trace("No explicit Facebook field name found for {}, so defaulting to the field name itself ({})",
409        field, field.getName());
410
411      facebookFieldName = field.getName();
412    }
413
414    return facebookFieldName;
415  }
416
417  /**
418   * Finds any Facebook JSON fields that are mapped to more than 1 Java field.
419   *
420   * @param fieldsWithAnnotation
421   *          Java fields annotated with the {@code Facebook} annotation.
422   * @return Any Facebook JSON fields that are mapped to more than 1 Java field.
423   */
424  protected Set<String> facebookFieldNamesWithMultipleMappings(
425      List<FieldWithAnnotation<Facebook>> fieldsWithAnnotation) {
426    Map<String, Integer> facebookFieldsNamesWithOccurrenceCount = new HashMap<>();
427
428    // Get a count of Facebook field name occurrences for each
429    // @Facebook-annotated field
430    fieldsWithAnnotation.forEach(field -> occurrenceCounter(facebookFieldsNamesWithOccurrenceCount, field));
431
432    // Pull out only those field names with multiple mappings
433    Set<String> facebookFieldNamesWithMultipleMappings = facebookFieldsNamesWithOccurrenceCount.entrySet().stream()
434      .filter(entry -> entry.getValue() > 1).map(Entry::getKey).collect(Collectors.toSet());
435
436    return unmodifiableSet(facebookFieldNamesWithMultipleMappings);
437  }
438
439  private void occurrenceCounter(Map<String, Integer> facebookFieldsNamesWithOccurrenceCount,
440      FieldWithAnnotation<Facebook> field) {
441    String fieldName = getFacebookFieldName(field);
442    int occurrenceCount = facebookFieldsNamesWithOccurrenceCount.getOrDefault(fieldName, 0);
443    facebookFieldsNamesWithOccurrenceCount.put(fieldName, occurrenceCount + 1);
444  }
445
446  @Override
447  public String toJson(Object object) {
448    return toJson(object, false);
449  }
450
451  @Override
452  public String toJson(Object object, boolean ignoreNullValuedProperties) {
453    JsonValue jsonObj = toJsonInternal(object, ignoreNullValuedProperties);
454    return jsonHelper.getStringFrom(jsonObj);
455  }
456
457  /**
458   * Recursively marshal the given {@code object} to JSON.
459   * <p>
460   * Used by {@link #toJson(Object)}.
461   *
462   * @param object
463   *          The object to marshal.
464   * @param ignoreNullValuedProperties
465   *          If this is {@code true}, no Javabean properties with {@code null} values will be included in the generated
466   *          JSON.
467   * @return JSON representation of the given {@code object}.
468   * @throws FacebookJsonMappingException
469   *           If an error occurs while marshaling to JSON.
470   */
471  protected JsonValue toJsonInternal(Object object, boolean ignoreNullValuedProperties) {
472    if (object == null) {
473      return Json.NULL;
474    }
475
476    if (object instanceof JsonValue) {
477      return (JsonValue) object;
478    }
479
480    if (object instanceof List<?>) {
481      return convertListToJsonArray((List<?>) object, ignoreNullValuedProperties);
482    }
483
484    if (object instanceof Map<?, ?>) {
485      return convertMapToJsonObject(object, ignoreNullValuedProperties);
486    }
487
488    if (isPrimitive(object)) {
489      return javaTypeToJsonValue(object);
490    }
491
492    if (object instanceof Optional) {
493      return convertOptionalToJsonValue((Optional) object, ignoreNullValuedProperties);
494    }
495
496    if (object instanceof BigInteger) {
497      return Json.value(((BigInteger) object).longValue());
498    }
499
500    if (object instanceof BigDecimal) {
501      return Json.value(((BigDecimal) object).doubleValue());
502    }
503
504    if (object instanceof Enum) {
505      Enum e = (Enum) object;
506      return Json.value(getAlternativeEnumValue(e).orElseGet(e::name));
507    }
508
509    if (object instanceof Date) {
510      return Json.value(DateUtils.toLongFormatFromDate((Date) object));
511    }
512
513    // We've passed the special-case bits, so let's try to marshal this as a
514    // plain old Javabean...
515
516    List<FieldWithAnnotation<Facebook>> fieldsWithAnnotation =
517        findFieldsWithAnnotation(object.getClass(), Facebook.class);
518
519    JsonObject jsonObject = new JsonObject();
520
521    // No longer throw an exception in this case. If there are multiple fields
522    // with the same @Facebook value, it's luck of the draw which is picked for
523    // JSON marshaling.
524    // TODO: A better implementation would query each duplicate-mapped field. If
525    // it has is a non-null value and the other duplicate values are null, use
526    // the non-null field.
527    Set<String> facebookFieldNamesWithMultipleMappings = facebookFieldNamesWithMultipleMappings(fieldsWithAnnotation);
528    if (!facebookFieldNamesWithMultipleMappings.isEmpty() && MAPPER_LOGGER.isDebugEnabled()) {
529      MAPPER_LOGGER.debug(
530        "Unable to convert to JSON because multiple @{} annotations for the same name are present: {}",
531        Facebook.class.getSimpleName(), facebookFieldNamesWithMultipleMappings);
532    }
533
534    for (FieldWithAnnotation<Facebook> fieldWithAnnotation : fieldsWithAnnotation) {
535      String facebookFieldName = getFacebookFieldName(fieldWithAnnotation);
536      fieldWithAnnotation.getField().setAccessible(true);
537
538      try {
539        Object fieldValue = fieldWithAnnotation.getField().get(object);
540
541        if (fieldValue instanceof Connection) {
542          continue;
543        }
544
545        if (!(ignoreNullValuedProperties
546            && (fieldValue == null || isEmptyOptional(fieldValue) || isEmptyCollectionOrMap(fieldValue)))) {
547          jsonObject.set(facebookFieldName, toJsonInternal(fieldValue, ignoreNullValuedProperties));
548        }
549      } catch (Exception e) {
550        throw new FacebookJsonMappingException(
551          "Unable to process field '" + facebookFieldName + "' for " + object.getClass(), e);
552      }
553    }
554
555    return jsonObject;
556  }
557
558  private boolean isEmptyOptional(Object fieldValue) {
559    return fieldValue instanceof Optional && !((Optional) fieldValue).isPresent();
560  }
561
562  private JsonArray convertListToJsonArray(List<?> objects, boolean ignoreNullValuedProperties) {
563    JsonArray jsonArray = new JsonArray();
564    objects.stream().map(o -> toJsonInternal(o, ignoreNullValuedProperties)).forEach(jsonArray::add);
565    return jsonArray;
566  }
567
568  private JsonObject convertMapToJsonObject(Object object, boolean ignoreNullValuedProperties) {
569    JsonObject jsonObject = new JsonObject();
570    for (Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
571      if (!(entry.getKey() instanceof String)) {
572        throw new FacebookJsonMappingException("Your Map keys must be of type " + String.class
573            + " in order to be converted to JSON.  Offending map is " + object);
574      }
575
576      try {
577        jsonObject.add((String) entry.getKey(), toJsonInternal(entry.getValue(), ignoreNullValuedProperties));
578      } catch (ParseException | IllegalArgumentException e) {
579        throw new FacebookJsonMappingException(
580          "Unable to process value '" + entry.getValue() + "' for key '" + entry.getKey() + "' in Map " + object, e);
581      }
582    }
583    return jsonObject;
584  }
585
586  private JsonValue convertOptionalToJsonValue(Optional<?> object, boolean ignoreNullValuedProperties) {
587    return toJsonInternal(object.orElse(null), ignoreNullValuedProperties);
588  }
589
590  /**
591   * Given a {@code json} value of something like {@code MyValue} or {@code 123} , return a representation of that value
592   * of type {@code type}.
593   * <p>
594   * This is to support non-legal JSON served up by Facebook for API calls like {@code Friends.get} (example result:
595   * {@code [222333,1240079]}).
596   *
597   * @param <T>
598   *          The Java type to map to.
599   * @param json
600   *          The non-legal JSON to map to the Java type.
601   * @param type
602   *          Type token.
603   * @return Java representation of {@code json}.
604   * @throws FacebookJsonMappingException
605   *           If an error occurs while mapping JSON to Java.
606   */
607  @SuppressWarnings("unchecked")
608  protected <T> T toPrimitiveJavaType(String json, Class<T> type) {
609
610    json = jsonHelper.cleanString(json);
611
612    if (typeIsString(type)) {
613      return (T) json;
614    }
615    if (typeIsInteger(type)) {
616      return (T) Integer.valueOf(json);
617    }
618    if (typeIsBoolean(type)) {
619      return (T) Boolean.valueOf(json);
620    }
621    if (typeIsLong(type)) {
622      return (T) Long.valueOf(json);
623    }
624    if (typeIsDouble(type)) {
625      return (T) Double.valueOf(json);
626    }
627    if (typeIsFloat(type)) {
628      return (T) Float.valueOf(json);
629    }
630    if (typeIsBigInteger(type)) {
631      return (T) new BigInteger(json);
632    }
633    if (typeIsBigDecimal(type)) {
634      return (T) new BigDecimal(json);
635    }
636
637    throw new FacebookJsonMappingException("Don't know how to map JSON to " + type
638        + ". Are you sure you're mapping to the right class?\nOffending JSON is '" + json + "'.");
639  }
640
641  /**
642   * Extracts JSON data for a field according to its {@code Facebook} annotation and returns it converted to the proper
643   * Java type.
644   *
645   * @param fieldWithAnnotation
646   *          The field/annotation pair which specifies what Java type to convert to.
647   * @param jsonObject
648   *          "Raw" JSON object to pull data from.
649   * @param facebookFieldName
650   *          Specifies what JSON field to pull "raw" data from.
651   * @return A
652   * @throws ParseException
653   *           If an error occurs while mapping JSON to Java.
654   * @throws FacebookJsonMappingException
655   *           If an error occurs while mapping JSON to Java.
656   */
657  protected Object toJavaType(FieldWithAnnotation<Facebook> fieldWithAnnotation, JsonObject jsonObject,
658      String facebookFieldName) {
659    Class<?> type = fieldWithAnnotation.getField().getType();
660    JsonValue rawValue = jsonObject.get(facebookFieldName);
661
662    // Short-circuit right off the bat if we've got a null value, but Optionals are created nevertheless.
663    if (jsonHelper.isNull(rawValue)) {
664      if (typeIsOptional(type)) {
665        return Optional.empty();
666      }
667      return null;
668    }
669
670    if (typeIsString(type)) {
671      /*
672       * Special handling here for better error checking.
673       *
674       * Since {@code JsonObject.getString()} will return literal JSON text even if it's _not_ a JSON string, we check
675       * the marshaled type and bail if needed. For example, calling {@code JsonObject.getString("results")} on the
676       * below JSON...
677       *
678       * <code> { "results":[ {"name":"Mark Allen"} ] } </code>
679       *
680       * ... would return the string {@code "[{"name":"Mark Allen"}]"} instead of throwing an error. So we throw the
681       * error ourselves.
682       *
683       * Per Antonello Naccarato, sometimes FB will return an empty JSON array instead of an empty string. Look for that
684       * here.
685       */
686      if (jsonHelper.isEmptyArray(rawValue)) {
687        MAPPER_LOGGER.trace("Coercing an empty JSON array to an empty string for {}", fieldWithAnnotation);
688
689        return "";
690      }
691
692      /*
693       * If the user wants a string, _always_ give her a string.
694       *
695       * This is useful if, for example, you've got a @Facebook-annotated string field that you'd like to have a numeric
696       * type shoved into.
697       *
698       * User beware: this will turn *anything* into a string, which might lead to results you don't expect.
699       */
700      return jsonHelper.getStringFrom(rawValue);
701    }
702
703    if (typeIsInteger(type)) {
704      return jsonHelper.getIntegerFrom(rawValue);
705    }
706    if (typeIsBoolean(type)) {
707      return jsonHelper.getBooleanFrom(rawValue);
708    }
709    if (typeIsLong(type)) {
710      return jsonHelper.getLongFrom(rawValue);
711    }
712    if (typeIsDouble(type)) {
713      return jsonHelper.getDoubleFrom(rawValue);
714    }
715    if (typeIsFloat(type)) {
716      return jsonHelper.getFloatFrom(rawValue);
717    }
718    if (typeIsBigInteger(type)) {
719      return jsonHelper.getBigIntegerFrom(rawValue);
720    }
721    if (typeIsBigDecimal(type)) {
722      return jsonHelper.getBigDecimalFrom(rawValue);
723    }
724    if (typeIsList(type)) {
725      return convertRawValueToList(rawValue, fieldWithAnnotation.getField());
726    }
727    if (typeIsMap(type)) {
728      return convertRawValueToMap(rawValue, fieldWithAnnotation.getField());
729    }
730
731    if (typeIsOptional(type)) {
732      return Optional.ofNullable(
733        toJavaObject(rawValue, getFirstParameterizedTypeArgument(fieldWithAnnotation.getField()), rawValue.toString()));
734    }
735
736    if (type.isEnum()) {
737      Optional<Enum> enumTypeOpt = convertRawValueToEnumType(type, rawValue);
738      if (enumTypeOpt.isPresent()) {
739        return enumTypeOpt.get();
740      }
741    }
742
743    if (typeIsDate(type)) {
744      return DateUtils.toDateFromLongFormat(jsonHelper.getStringFrom(rawValue));
745    }
746
747    if (Connection.class.equals(type)) {
748      Optional<Connection> createdConnectionOpt = convertRawValueToConnection(fieldWithAnnotation, rawValue);
749      if (createdConnectionOpt.isPresent()) {
750        return createdConnectionOpt.get();
751      }
752    }
753
754    String rawValueAsString = jsonHelper.getStringFrom(rawValue);
755
756    // Some other type - recurse into it
757    JsonValue nextValue = rawValue;
758    if (Comments.class.isAssignableFrom(type) && rawValue instanceof JsonArray) {
759      MAPPER_LOGGER.debug(
760        "Encountered comment array '{}' but expected a {} object instead.  Working around that by coercing "
761            + "into an empty {} instance...",
762        rawValueAsString, Comments.class.getSimpleName(), Comments.class.getSimpleName());
763      JsonObject workaroundJsonObject = new JsonObject();
764      workaroundJsonObject.add("total_count", 0);
765      workaroundJsonObject.add("data", new JsonArray());
766      nextValue = workaroundJsonObject;
767      rawValueAsString = workaroundJsonObject.toString();
768    }
769    return toJavaObject(nextValue, type, rawValueAsString);
770  }
771
772  private Optional<Connection> convertRawValueToConnection(FieldWithAnnotation<Facebook> fieldWithAnnotation,
773      JsonValue rawValue) {
774    if (null != facebookClient) {
775      String rawJson = jsonHelper.getStringFrom(rawValue);
776      Class<?> elementType = getFirstParameterizedTypeArgument(fieldWithAnnotation.getField());
777      return Optional.of(facebookClient.createConnection(rawJson, elementType));
778    } else {
779      MAPPER_LOGGER.warn(
780        "Skipping java field {}, because it has the type Connection, but the given facebook client is null",
781        fieldWithAnnotation.getField().getName());
782    }
783    return Optional.empty();
784  }
785
786  private Optional<Enum> convertRawValueToEnumType(Class<?> type, JsonValue rawValue) {
787    Class<? extends Enum> enumType = type.asSubclass(Enum.class);
788    Map<String, Enum> annotatedEnumMapping = new HashMap<>();
789    if (enumType.getEnumConstants() != null) {
790      for (Enum e : enumType.getEnumConstants()) {
791        getAlternativeEnumValue(e).ifPresent(s -> annotatedEnumMapping.put(s, e));
792      }
793    }
794
795    Enum e = annotatedEnumMapping.get(rawValue.asString());
796    if (e != null) {
797      return Optional.of(e);
798    } else {
799      MAPPER_LOGGER.debug(
800        "No suitable annotated enum constant found for string {} and enum {}, use default enum detection.",
801        rawValue.asString(), enumType.getName());
802    }
803
804    try {
805      return Optional.of(Enum.valueOf(enumType, rawValue.asString()));
806    } catch (IllegalArgumentException iae) {
807      MAPPER_LOGGER.debug("Cannot map string {} to enum {}, try fallback toUpperString next...", rawValue.asString(),
808        enumType.getName());
809    }
810    try {
811      return Optional.of(Enum.valueOf(enumType, rawValue.asString().toUpperCase()));
812    } catch (IllegalArgumentException iae) {
813      MAPPER_LOGGER.debug("Mapping string {} to enum {} not possible", rawValue.asString(), enumType.getName());
814    }
815    return Optional.empty();
816  }
817
818  private Optional<String> getAlternativeEnumValue(Enum e) {
819    try {
820      Field f = e.getClass().getField(e.toString());
821      Facebook a = f.getAnnotation(Facebook.class);
822      if (a != null && !a.value().isEmpty()) {
823        return Optional.of(a.value());
824      }
825    } catch (NoSuchFieldException ex) {
826      MAPPER_LOGGER.debug("Enum constant without annotation, skip annotation value detection for {}", e);
827    }
828    return Optional.empty();
829  }
830
831  private Map convertRawValueToMap(JsonValue jsonValue, Field field) {
832    Class<?> firstParam = getFirstParameterizedTypeArgument(field);
833    if (!typeIsString(firstParam)) {
834      throw new FacebookJsonMappingException("The java type map needs to have a 'String' key, but is " + firstParam);
835    }
836
837    Class<?> secondParam = getSecondParameterizedTypeArgument(field);
838
839    if (jsonValue != null && jsonValue.isObject()) {
840      JsonObject jsonObject = jsonValue.asObject();
841      Map<String, Object> map = new HashMap<>();
842      for (String key : jsonObject.names()) {
843        map.put(key, toJavaObject(jsonObject.get(key), secondParam));
844      }
845      return map;
846    }
847
848    // @TODO: return emptyMap here, to allow the devs to go on without null check (v2024)
849    return null;
850  }
851
852  private List<?> convertRawValueToList(JsonValue rawJson, Field field) {
853    Type type = getParameterizedTypeArgument(field.getGenericType(), 0);
854
855    if (type == null) {
856      throw new FacebookJsonMappingException("No generic type specified for field: " + field.getName());
857    }
858
859    return convertRawValueToList(rawJson, type);
860  }
861
862  private List<?> convertRawValueToList(JsonValue rawJson, Type type) {
863    if (type.equals(List.class)) {
864      throw new FacebookJsonMappingException("You must specify the generic type for mapping");
865    }
866
867    if (type instanceof Class<?>) {
868      return toJavaList(rawJson, (Class<?>) type);
869    }
870
871    if (!(type instanceof ParameterizedType)) {
872      throw new FacebookJsonMappingException("Unsupported type: " + type);
873    }
874
875    ParameterizedType paramType = (ParameterizedType) type;
876    if (!paramType.getRawType().equals(List.class)) {
877      throw new FacebookJsonMappingException("Type must be a List, found: " + paramType.getRawType());
878    }
879
880    Type innerType = paramType.getActualTypeArguments()[0];
881
882    try {
883      JsonArray jsonArray = rawJson.asArray();
884      List<Object> result = new ArrayList<>(jsonArray.size());
885
886      for (JsonValue jsonValue : jsonArray) {
887        Object value = convertRawValueToList(jsonValue, innerType);
888        result.add(value);
889      }
890      return unmodifiableList(result);
891    } catch (FacebookJsonMappingException e) {
892      throw e;
893    } catch (Exception e) {
894      throw new FacebookJsonMappingException(
895        "Unable to convert Facebook response JSON to a list of " + innerType + " instances", e);
896    }
897  }
898
899  private JsonValue javaTypeToJsonValue(Object object) {
900    if (object == null) {
901      return Json.NULL;
902    }
903
904    Class<?> type = object.getClass();
905
906    if (typeIsString(type)) {
907      return Json.value((String) object);
908    }
909
910    if (typeIsInteger(type)) {
911      return Json.value((Integer) object);
912    }
913
914    if (typeIsBoolean(type)) {
915      return Json.value((Boolean) object);
916    }
917
918    if (typeIsLong(type)) {
919      return Json.value((Long) object);
920    }
921
922    if (typeIsDouble(type)) {
923      return Json.value((Double) object);
924    }
925
926    if (typeIsFloat(type)) {
927      return Json.value((Float) object);
928    }
929
930    if (typeIsByte(type)) {
931      return Json.value((Byte) object);
932    }
933
934    if (typeIsShort(type)) {
935      return Json.value((Short) object);
936    }
937
938    if (typeIsCharacter(type)) {
939      return Json.value(Character.toString((Character) object));
940    }
941
942    return Json.NULL;
943
944  }
945
946  private boolean typeIsCharacter(Class<?> type) {
947    return Character.class.equals(type) || Character.TYPE.equals(type);
948  }
949
950  private boolean typeIsShort(Class<?> type) {
951    return Short.class.equals(type) || Short.TYPE.equals(type);
952  }
953
954  private boolean typeIsByte(Class<?> type) {
955    return Byte.class.equals(type) || Byte.TYPE.equals(type);
956  }
957
958  private boolean typeIsFloat(Class<?> type) {
959    return Float.class.equals(type) || Float.TYPE.equals(type);
960  }
961
962  private boolean typeIsLong(Class<?> type) {
963    return Long.class.equals(type) || Long.TYPE.equals(type);
964  }
965
966  private boolean typeIsInteger(Class<?> type) {
967    return Integer.class.equals(type) || Integer.TYPE.equals(type);
968  }
969
970  private boolean typeIsBigDecimal(Class<?> type) {
971    return BigDecimal.class.equals(type);
972  }
973
974  private boolean typeIsBigInteger(Class<?> type) {
975    return BigInteger.class.equals(type);
976  }
977
978  private boolean typeIsDouble(Class<?> type) {
979    return Double.class.equals(type) || Double.TYPE.equals(type);
980  }
981
982  private boolean typeIsBoolean(Class<?> type) {
983    return Boolean.class.equals(type) || Boolean.TYPE.equals(type);
984  }
985
986  private boolean typeIsString(Class<?> type) {
987    return String.class.equals(type);
988  }
989
990  private boolean typeIsOptional(Class<?> type) {
991    return Optional.class.equals(type);
992  }
993
994  private boolean typeIsMap(Class<?> type) {
995    return Map.class.equals(type);
996  }
997
998  private boolean typeIsList(Class<?> type) {
999    return List.class.equals(type);
1000  }
1001
1002  private boolean typeIsDate(Class<?> type) {
1003    return Date.class.equals(type);
1004  }
1005}