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}