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