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