Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *    http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.facebook.model;
18
 
19
import com.facebook.FacebookGraphObjectException;
20
import com.facebook.internal.Utility;
21
import com.facebook.internal.Validate;
22
import org.json.JSONArray;
23
import org.json.JSONException;
24
import org.json.JSONObject;
25
 
26
import java.lang.reflect.*;
27
import java.text.ParseException;
28
import java.text.SimpleDateFormat;
29
import java.util.*;
30
 
31
/**
32
 * GraphObject is the primary interface used by the Facebook SDK for Android to represent objects in the Facebook
33
 * Social Graph and the Facebook Open Graph (OG). It is the base interface for all typed access to graph objects
34
 * in the SDK. No concrete classes implement GraphObject or its derived interfaces. Rather, they are implemented as
35
 * proxies (see the {@link com.facebook.model.GraphObject.Factory Factory} class) that provide strongly-typed property
36
 * getters and setters to access the underlying data. Since the primary use case for graph objects is sending and
37
 * receiving them over the wire to/from Facebook services, they are represented as JSONObjects. No validation is done
38
 * that a graph object is actually of a specific type -- any graph object can be treated as any GraphObject-derived
39
 * interface, and the presence or absence of specific properties determines its suitability for use as that
40
 * particular type of object.
41
 * <br/>
42
 */
43
public interface GraphObject {
44
    /**
45
     * Returns a new proxy that treats this graph object as a different GraphObject-derived type.
46
     * @param graphObjectClass the type of GraphObject to return
47
     * @return a new instance of the GraphObject-derived-type that references the same underlying data
48
     */
49
    <T extends GraphObject> T cast(Class<T> graphObjectClass);
50
 
51
    /**
52
     * Returns a Java Collections map of names and properties.  Modifying the returned map modifies the
53
     * inner JSON representation.
54
     * @return a Java Collections map representing the GraphObject state
55
     */
56
    Map<String, Object> asMap();
57
 
58
    /**
59
     * Gets the underlying JSONObject representation of this graph object.
60
     * @return the underlying JSONObject representation of this graph object
61
     */
62
    JSONObject getInnerJSONObject();
63
 
64
    /**
65
     * Gets a property of the GraphObject
66
     * @param propertyName the name of the property to get
67
     * @return the value of the named property
68
     */
69
    Object getProperty(String propertyName);
70
 
71
    /**
72
     * Gets a property of the GraphObject, cast to a particular GraphObject-derived interface. This gives some of
73
     * the benefits of having a property getter defined to return a GraphObject-derived type without requiring
74
     * explicit definition of an interface to define the getter.
75
     * @param propertyName the name of the property to get
76
     * @param graphObjectClass the GraphObject-derived interface to cast the property to
77
     * @return
78
     */
79
    <T extends GraphObject> T getPropertyAs(String propertyName, Class<T> graphObjectClass);
80
 
81
    /**
82
     * Gets a property of the GraphObject, cast to a a list of instances of a particular GraphObject-derived interface.
83
     * This gives some of the benefits of having a property getter defined to return a GraphObject-derived type without
84
     * requiring explicit definition of an interface to define the getter.
85
     * @param propertyName the name of the property to get
86
     * @param graphObjectClass the GraphObject-derived interface to cast the property to a list of
87
     * @return
88
     */
89
    <T extends GraphObject> GraphObjectList<T> getPropertyAsList(String propertyName, Class<T> graphObjectClass);
90
 
91
    /**
92
     * Sets a property of the GraphObject
93
     * @param propertyName the name of the property to set
94
     * @param propertyValue the value of the named property to set
95
     */
96
    void setProperty(String propertyName, Object propertyValue);
97
 
98
    /**
99
     * Removes a property of the GraphObject
100
     * @param propertyName the name of the property to remove
101
     */
102
    void removeProperty(String propertyName);
103
 
104
    /**
105
     * Creates proxies that implement GraphObject, GraphObjectList, and their derived types. These proxies allow access
106
     * to underlying collections and name/value property bags via strongly-typed property getters and setters.
107
     * <p/>
108
     * This supports get/set properties that use primitive types, JSON types, Date, other GraphObject types, Iterable,
109
     * Collection, List, and GraphObjectList.
110
     */
111
    final class Factory {
112
        private static final HashSet<Class<?>> verifiedGraphObjectClasses = new HashSet<Class<?>>();
113
        private static final SimpleDateFormat[] dateFormats = new SimpleDateFormat[] {
114
                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
115
                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US), new SimpleDateFormat("yyyy-MM-dd", Locale.US), };
116
 
117
        // No objects of this type should exist.
118
        private Factory() {
119
        }
120
 
121
        /**
122
         * Creates a GraphObject proxy that provides typed access to the data in an underlying JSONObject.
123
         * @param json the JSONObject containing the data to be exposed
124
         * @return a GraphObject that represents the underlying data
125
         *
126
         * @throws com.facebook.FacebookException
127
         *            If the passed in Class is not a valid GraphObject interface
128
         */
129
        public static GraphObject create(JSONObject json) {
130
            return create(json, GraphObject.class);
131
        }
132
 
133
        /**
134
         * Creates a GraphObject-derived proxy that provides typed access to the data in an underlying JSONObject.
135
         * @param json the JSONObject containing the data to be exposed
136
         * @param graphObjectClass the GraphObject-derived type to return
137
         * @return a graphObjectClass that represents the underlying data
138
         *
139
         * @throws com.facebook.FacebookException
140
         *            If the passed in Class is not a valid GraphObject interface
141
         */
142
        public static <T extends GraphObject> T create(JSONObject json, Class<T> graphObjectClass) {
143
            return createGraphObjectProxy(graphObjectClass, json);
144
        }
145
 
146
        /**
147
         * Creates a GraphObject proxy that initially contains no data.
148
         * @return a GraphObject with no data
149
         *
150
         * @throws com.facebook.FacebookException
151
         *            If the passed in Class is not a valid GraphObject interface
152
         */
153
        public static GraphObject create() {
154
            return create(GraphObject.class);
155
        }
156
 
157
        /**
158
         * Creates a GraphObject-derived proxy that initially contains no data.
159
         * @param graphObjectClass the GraphObject-derived type to return
160
         * @return a graphObjectClass with no data
161
         *
162
         * @throws com.facebook.FacebookException
163
         *            If the passed in Class is not a valid GraphObject interface
164
         */
165
        public static <T extends GraphObject> T create(Class<T> graphObjectClass) {
166
            return createGraphObjectProxy(graphObjectClass, new JSONObject());
167
        }
168
 
169
        /**
170
         * Determines if two GraphObjects represent the same underlying graph object, based on their IDs.
171
         * @param a a graph object
172
         * @param b another graph object
173
         * @return true if both graph objects have an ID and it is the same ID, false otherwise
174
         */
175
        public static boolean hasSameId(GraphObject a, GraphObject b) {
176
            if (a == null || b == null || !a.asMap().containsKey("id") || !b.asMap().containsKey("id")) {
177
                return false;
178
            }
179
            if (a.equals(b)) {
180
                return true;
181
            }
182
            Object idA = a.getProperty("id");
183
            Object idB = b.getProperty("id");
184
            if (idA == null || idB == null || !(idA instanceof String) || !(idB instanceof String)) {
185
                return false;
186
            }
187
            return idA.equals(idB);
188
        }
189
 
190
        /**
191
         * Creates a GraphObjectList-derived proxy that provides typed access to the data in an underlying JSONArray.
192
         * @param array the JSONArray containing the data to be exposed
193
         * @param graphObjectClass the GraphObject-derived type to return
194
         * @return a graphObjectClass that represents the underlying data
195
         *
196
         * @throws com.facebook.FacebookException
197
         *            If the passed in Class is not a valid GraphObject interface
198
         */
199
        public static <T> GraphObjectList<T> createList(JSONArray array, Class<T> graphObjectClass) {
200
            return new GraphObjectListImpl<T>(array, graphObjectClass);
201
        }
202
 
203
        /**
204
         * Creates a GraphObjectList-derived proxy that initially contains no data.
205
         * @param graphObjectClass the GraphObject-derived type to return
206
         * @return a GraphObjectList with no data
207
         *
208
         * @throws com.facebook.FacebookException
209
         *            If the passed in Class is not a valid GraphObject interface
210
         */
211
        public static <T> GraphObjectList<T> createList(Class<T> graphObjectClass) {
212
            return createList(new JSONArray(), graphObjectClass);
213
        }
214
 
215
        private static <T extends GraphObject> T createGraphObjectProxy(Class<T> graphObjectClass, JSONObject state) {
216
            verifyCanProxyClass(graphObjectClass);
217
 
218
            Class<?>[] interfaces = new Class<?>[] { graphObjectClass };
219
            GraphObjectProxy graphObjectProxy = new GraphObjectProxy(state, graphObjectClass);
220
 
221
            @SuppressWarnings("unchecked")
222
            T graphObject = (T) Proxy.newProxyInstance(GraphObject.class.getClassLoader(), interfaces, graphObjectProxy);
223
 
224
            return graphObject;
225
        }
226
 
227
        private static Map<String, Object> createGraphObjectProxyForMap(JSONObject state) {
228
            Class<?>[] interfaces = new Class<?>[]{Map.class};
229
            GraphObjectProxy graphObjectProxy = new GraphObjectProxy(state, Map.class);
230
 
231
            @SuppressWarnings("unchecked")
232
            Map<String, Object> graphObject = (Map<String, Object>) Proxy
233
                    .newProxyInstance(GraphObject.class.getClassLoader(), interfaces, graphObjectProxy);
234
 
235
            return graphObject;
236
        }
237
 
238
        private static synchronized <T extends GraphObject> boolean hasClassBeenVerified(Class<T> graphObjectClass) {
239
            return verifiedGraphObjectClasses.contains(graphObjectClass);
240
        }
241
 
242
        private static synchronized <T extends GraphObject> void recordClassHasBeenVerified(Class<T> graphObjectClass) {
243
            verifiedGraphObjectClasses.add(graphObjectClass);
244
        }
245
 
246
        private static <T extends GraphObject> void verifyCanProxyClass(Class<T> graphObjectClass) {
247
            if (hasClassBeenVerified(graphObjectClass)) {
248
                return;
249
            }
250
 
251
            if (!graphObjectClass.isInterface()) {
252
                throw new FacebookGraphObjectException("Factory can only wrap interfaces, not class: "
253
                        + graphObjectClass.getName());
254
            }
255
 
256
            Method[] methods = graphObjectClass.getMethods();
257
            for (Method method : methods) {
258
                String methodName = method.getName();
259
                int parameterCount = method.getParameterTypes().length;
260
                Class<?> returnType = method.getReturnType();
261
                boolean hasPropertyNameOverride = method.isAnnotationPresent(PropertyName.class);
262
 
263
                if (method.getDeclaringClass().isAssignableFrom(GraphObject.class)) {
264
                    // Don't worry about any methods from GraphObject or one of its base classes.
265
                    continue;
266
                } else if (parameterCount == 1 && returnType == Void.TYPE) {
267
                    if (hasPropertyNameOverride) {
268
                        // If a property override is present, it MUST be valid. We don't fallback
269
                        // to using the method name
270
                        if (!Utility.isNullOrEmpty(method.getAnnotation(PropertyName.class).value())) {
271
                            continue;
272
                        }
273
                    } else if (methodName.startsWith("set") && methodName.length() > 3) {
274
                        // Looks like a valid setter
275
                        continue;
276
                    }
277
                } else if (parameterCount == 0 && returnType != Void.TYPE) {
278
                    if (hasPropertyNameOverride) {
279
                        // If a property override is present, it MUST be valid. We don't fallback
280
                        // to using the method name
281
                        if (!Utility.isNullOrEmpty(method.getAnnotation(PropertyName.class).value())) {
282
                            continue;
283
                        }
284
                    } else if (methodName.startsWith("get") && methodName.length() > 3) {
285
                        // Looks like a valid getter
286
                        continue;
287
                    }
288
                }
289
 
290
                throw new FacebookGraphObjectException("Factory can't proxy method: " + method.toString());
291
            }
292
 
293
            recordClassHasBeenVerified(graphObjectClass);
294
        }
295
 
296
        // If expectedType is a generic type, expectedTypeAsParameterizedType must be provided in order to determine
297
        // generic parameter types.
298
        static <U> U coerceValueToExpectedType(Object value, Class<U> expectedType,
299
                ParameterizedType expectedTypeAsParameterizedType) {
300
            if (value == null) {
301
                if (boolean.class.equals(expectedType)) {
302
                    @SuppressWarnings("unchecked")
303
                    U result = (U) (Boolean) false;
304
                    return result;
305
                } else if (char.class.equals(expectedType)) {
306
                    @SuppressWarnings("unchecked")
307
                    U result = (U) (Character) '\0';
308
                    return result;
309
                } else if (expectedType.isPrimitive()) {
310
                    @SuppressWarnings("unchecked")
311
                    U result = (U) (Number) 0;
312
                    return result;
313
                } else {
314
                    return null;
315
                }
316
            }
317
 
318
            Class<?> valueType = value.getClass();
319
            if (expectedType.isAssignableFrom(valueType)) {
320
                @SuppressWarnings("unchecked")
321
                U result = (U) value;
322
                return result;
323
            }
324
 
325
            if (expectedType.isPrimitive()) {
326
                // If the result is a primitive, let the runtime succeed or fail at unboxing it.
327
                @SuppressWarnings("unchecked")
328
                U result = (U) value;
329
                return result;
330
            }
331
 
332
            if (GraphObject.class.isAssignableFrom(expectedType)) {
333
                @SuppressWarnings("unchecked")
334
                Class<? extends GraphObject> graphObjectClass = (Class<? extends GraphObject>) expectedType;
335
 
336
                // We need a GraphObject, but we don't have one.
337
                if (JSONObject.class.isAssignableFrom(valueType)) {
338
                    // We can wrap a JSONObject as a GraphObject.
339
                    @SuppressWarnings("unchecked")
340
                    U result = (U) createGraphObjectProxy(graphObjectClass, (JSONObject) value);
341
                    return result;
342
                } else if (GraphObject.class.isAssignableFrom(valueType)) {
343
                    // We can cast a GraphObject-derived class to another GraphObject-derived class.
344
                    @SuppressWarnings("unchecked")
345
                    U result = (U) ((GraphObject) value).cast(graphObjectClass);
346
                    return result;
347
                } else {
348
                    throw new FacebookGraphObjectException("Can't create GraphObject from " + valueType.getName());
349
                }
350
            } else if (Iterable.class.equals(expectedType) || Collection.class.equals(expectedType)
351
                    || List.class.equals(expectedType) || GraphObjectList.class.equals(expectedType)) {
352
                if (expectedTypeAsParameterizedType == null) {
353
                    throw new FacebookGraphObjectException("can't infer generic type of: " + expectedType.toString());
354
                }
355
 
356
                Type[] actualTypeArguments = expectedTypeAsParameterizedType.getActualTypeArguments();
357
 
358
                if (actualTypeArguments == null || actualTypeArguments.length != 1
359
                        || !(actualTypeArguments[0] instanceof Class<?>)) {
360
                    throw new FacebookGraphObjectException(
361
                            "Expect collection properties to be of a type with exactly one generic parameter.");
362
                }
363
                Class<?> collectionGenericArgument = (Class<?>) actualTypeArguments[0];
364
 
365
                if (JSONArray.class.isAssignableFrom(valueType)) {
366
                    JSONArray jsonArray = (JSONArray) value;
367
                    @SuppressWarnings("unchecked")
368
                    U result = (U) createList(jsonArray, collectionGenericArgument);
369
                    return result;
370
                } else {
371
                    throw new FacebookGraphObjectException("Can't create Collection from " + valueType.getName());
372
                }
373
            } else if (String.class.equals(expectedType)) {
374
                if (Double.class.isAssignableFrom(valueType) ||
375
                        Float.class.isAssignableFrom(valueType)) {
376
                    @SuppressWarnings("unchecked")
377
                    U result = (U) String.format("%f", value);
378
                    return result;
379
                } else if (Number.class.isAssignableFrom(valueType)) {
380
                    @SuppressWarnings("unchecked")
381
                    U result = (U) String.format("%d", value);
382
                    return result;
383
                }
384
            } else if (Date.class.equals(expectedType)) {
385
                if (String.class.isAssignableFrom(valueType)) {
386
                    for (SimpleDateFormat format : dateFormats) {
387
                        try {
388
                            Date date = format.parse((String) value);
389
                            if (date != null) {
390
                                @SuppressWarnings("unchecked")
391
                                U result = (U) date;
392
                                return result;
393
                            }
394
                        } catch (ParseException e) {
395
                            // Keep going.
396
                        }
397
                    }
398
                }
399
            }
400
            throw new FacebookGraphObjectException("Can't convert type" + valueType.getName() + " to "
401
                    + expectedType.getName());
402
        }
403
 
404
        static String convertCamelCaseToLowercaseWithUnderscores(String string) {
405
            string = string.replaceAll("([a-z])([A-Z])", "$1_$2");
406
            return string.toLowerCase(Locale.US);
407
        }
408
 
409
        private static Object getUnderlyingJSONObject(Object obj) {
410
            if (obj == null) {
411
                return null;
412
            }
413
 
414
            Class<?> objClass = obj.getClass();
415
            if (GraphObject.class.isAssignableFrom(objClass)) {
416
                GraphObject graphObject = (GraphObject) obj;
417
                return graphObject.getInnerJSONObject();
418
            } else if (GraphObjectList.class.isAssignableFrom(objClass)) {
419
                GraphObjectList<?> graphObjectList = (GraphObjectList<?>) obj;
420
                return graphObjectList.getInnerJSONArray();
421
            } else if (Iterable.class.isAssignableFrom(objClass)) {
422
                JSONArray jsonArray = new JSONArray();
423
                Iterable<?> iterable = (Iterable<?>) obj;
424
                for (Object o : iterable ) {
425
                    if (GraphObject.class.isAssignableFrom(o.getClass())) {
426
                        jsonArray.put(((GraphObject)o).getInnerJSONObject());
427
                    } else {
428
                        jsonArray.put(o);
429
                    }
430
                }
431
                return jsonArray;
432
            }
433
            return obj;
434
        }
435
 
436
        private abstract static class ProxyBase<STATE> implements InvocationHandler {
437
            // Pre-loaded Method objects for the methods in java.lang.Object
438
            private static final String EQUALS_METHOD = "equals";
439
            private static final String TOSTRING_METHOD = "toString";
440
 
441
            protected final STATE state;
442
 
443
            protected ProxyBase(STATE state) {
444
                this.state = state;
445
            }
446
 
447
            // Declared to return Object just to simplify implementation of proxy helpers.
448
            protected final Object throwUnexpectedMethodSignature(Method method) {
449
                throw new FacebookGraphObjectException(getClass().getName() + " got an unexpected method signature: "
450
                        + method.toString());
451
            }
452
 
453
            protected final Object proxyObjectMethods(Object proxy, Method method, Object[] args) throws Throwable {
454
                String methodName = method.getName();
455
                if (methodName.equals(EQUALS_METHOD)) {
456
                    Object other = args[0];
457
 
458
                    if (other == null) {
459
                        return false;
460
                    }
461
 
462
                    InvocationHandler handler = Proxy.getInvocationHandler(other);
463
                    if (!(handler instanceof GraphObjectProxy)) {
464
                        return false;
465
                    }
466
                    GraphObjectProxy otherProxy = (GraphObjectProxy) handler;
467
                    return this.state.equals(otherProxy.state);
468
                } else if (methodName.equals(TOSTRING_METHOD)) {
469
                    return toString();
470
                }
471
 
472
                // For others, just defer to the implementation object.
473
                return method.invoke(this.state, args);
474
            }
475
 
476
        }
477
 
478
        private final static class GraphObjectProxy extends ProxyBase<JSONObject> {
479
            private static final String CLEAR_METHOD = "clear";
480
            private static final String CONTAINSKEY_METHOD = "containsKey";
481
            private static final String CONTAINSVALUE_METHOD = "containsValue";
482
            private static final String ENTRYSET_METHOD = "entrySet";
483
            private static final String GET_METHOD = "get";
484
            private static final String ISEMPTY_METHOD = "isEmpty";
485
            private static final String KEYSET_METHOD = "keySet";
486
            private static final String PUT_METHOD = "put";
487
            private static final String PUTALL_METHOD = "putAll";
488
            private static final String REMOVE_METHOD = "remove";
489
            private static final String SIZE_METHOD = "size";
490
            private static final String VALUES_METHOD = "values";
491
            private static final String CAST_METHOD = "cast";
492
            private static final String CASTTOMAP_METHOD = "asMap";
493
            private static final String GETPROPERTY_METHOD = "getProperty";
494
            private static final String GETPROPERTYAS_METHOD = "getPropertyAs";
495
            private static final String GETPROPERTYASLIST_METHOD = "getPropertyAsList";
496
            private static final String SETPROPERTY_METHOD = "setProperty";
497
            private static final String REMOVEPROPERTY_METHOD = "removeProperty";
498
            private static final String GETINNERJSONOBJECT_METHOD = "getInnerJSONObject";
499
 
500
            private final Class<?> graphObjectClass;
501
 
502
            public GraphObjectProxy(JSONObject state, Class<?> graphObjectClass) {
503
                super(state);
504
                this.graphObjectClass = graphObjectClass;
505
            }
506
 
507
            @Override
508
            public String toString() {
509
                return String.format("GraphObject{graphObjectClass=%s, state=%s}", graphObjectClass.getSimpleName(), state);
510
            }
511
 
512
            @Override
513
            public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
514
                Class<?> declaringClass = method.getDeclaringClass();
515
 
516
                if (declaringClass == Object.class) {
517
                    return proxyObjectMethods(proxy, method, args);
518
                } else if (declaringClass == Map.class) {
519
                    return proxyMapMethods(method, args);
520
                } else if (declaringClass == GraphObject.class) {
521
                    return proxyGraphObjectMethods(proxy, method, args);
522
                } else if (GraphObject.class.isAssignableFrom(declaringClass)) {
523
                    return proxyGraphObjectGettersAndSetters(method, args);
524
                }
525
 
526
                return throwUnexpectedMethodSignature(method);
527
            }
528
 
529
            private final Object proxyMapMethods(Method method, Object[] args) {
530
                String methodName = method.getName();
531
                if (methodName.equals(CLEAR_METHOD)) {
532
                    JsonUtil.jsonObjectClear(this.state);
533
                    return null;
534
                } else if (methodName.equals(CONTAINSKEY_METHOD)) {
535
                    return this.state.has((String) args[0]);
536
                } else if (methodName.equals(CONTAINSVALUE_METHOD)) {
537
                    return JsonUtil.jsonObjectContainsValue(this.state, args[0]);
538
                } else if (methodName.equals(ENTRYSET_METHOD)) {
539
                    return JsonUtil.jsonObjectEntrySet(this.state);
540
                } else if (methodName.equals(GET_METHOD)) {
541
                    return this.state.opt((String) args[0]);
542
                } else if (methodName.equals(ISEMPTY_METHOD)) {
543
                    return this.state.length() == 0;
544
                } else if (methodName.equals(KEYSET_METHOD)) {
545
                    return JsonUtil.jsonObjectKeySet(this.state);
546
                } else if (methodName.equals(PUT_METHOD)) {
547
                    return setJSONProperty(args);
548
                } else if (methodName.equals(PUTALL_METHOD)) {
549
                    Map<String, Object> map = null;
550
                    if (args[0] instanceof Map<?, ?>) {
551
                        @SuppressWarnings("unchecked")
552
                        Map<String, Object> castMap = (Map<String, Object>) args[0];
553
                        map = castMap;
554
                    } else if (args[0] instanceof GraphObject) {
555
                        map = ((GraphObject) args[0]).asMap();
556
                    } else {
557
                        return null;
558
                    }
559
                    JsonUtil.jsonObjectPutAll(this.state, map);
560
                    return null;
561
                } else if (methodName.equals(REMOVE_METHOD)) {
562
                    this.state.remove((String) args[0]);
563
                    return null;
564
                } else if (methodName.equals(SIZE_METHOD)) {
565
                    return this.state.length();
566
                } else if (methodName.equals(VALUES_METHOD)) {
567
                    return JsonUtil.jsonObjectValues(this.state);
568
                }
569
 
570
                return throwUnexpectedMethodSignature(method);
571
            }
572
 
573
            private final Object proxyGraphObjectMethods(Object proxy, Method method, Object[] args) {
574
                String methodName = method.getName();
575
                if (methodName.equals(CAST_METHOD)) {
576
                    @SuppressWarnings("unchecked")
577
                    Class<? extends GraphObject> graphObjectClass = (Class<? extends GraphObject>) args[0];
578
 
579
                    if (graphObjectClass != null &&
580
                            graphObjectClass.isAssignableFrom(this.graphObjectClass)) {
581
                        return proxy;
582
                    }
583
                    return Factory.createGraphObjectProxy(graphObjectClass, this.state);
584
                } else if (methodName.equals(GETINNERJSONOBJECT_METHOD)) {
585
                    InvocationHandler handler = Proxy.getInvocationHandler(proxy);
586
                    GraphObjectProxy otherProxy = (GraphObjectProxy) handler;
587
                    return otherProxy.state;
588
                } else if (methodName.equals(CASTTOMAP_METHOD)) {
589
                    return Factory.createGraphObjectProxyForMap(this.state);
590
                } else if (methodName.equals(GETPROPERTY_METHOD)) {
591
                    return state.opt((String) args[0]);
592
                } else if (methodName.equals(GETPROPERTYAS_METHOD)) {
593
                    Object value = state.opt((String) args[0]);
594
                    Class<?> expectedType = (Class<?>) args[1];
595
 
596
                    return coerceValueToExpectedType(value, expectedType, null);
597
                } else if (methodName.equals(GETPROPERTYASLIST_METHOD)) {
598
                    Object value = state.opt((String) args[0]);
599
                    final Class<?> expectedType = (Class<?>) args[1];
600
 
601
                    ParameterizedType parameterizedType = new ParameterizedType() {
602
                        @Override
603
                        public Type[] getActualTypeArguments() {
604
                            return new Type[]{ expectedType };
605
                        }
606
 
607
                        @Override
608
                        public Type getOwnerType() {
609
                            return null;
610
                        }
611
 
612
                        @Override
613
                        public Type getRawType() {
614
                            return GraphObjectList.class;
615
                        }
616
                    };
617
                    return coerceValueToExpectedType(value, GraphObjectList.class, parameterizedType);
618
                } else if (methodName.equals(SETPROPERTY_METHOD)) {
619
                    return setJSONProperty(args);
620
                } else if (methodName.equals(REMOVEPROPERTY_METHOD)) {
621
                    this.state.remove((String) args[0]);
622
                    return null;
623
                }
624
 
625
                return throwUnexpectedMethodSignature(method);
626
            }
627
 
628
            private Object createGraphObjectsFromParameters(CreateGraphObject createGraphObject, Object value) {
629
                if (createGraphObject != null &&
630
                        !Utility.isNullOrEmpty(createGraphObject.value())) {
631
                    String propertyName = createGraphObject.value();
632
                    if (List.class.isAssignableFrom(value.getClass())) {
633
                        GraphObjectList<GraphObject> graphObjects = GraphObject.Factory.createList(GraphObject.class);
634
                        @SuppressWarnings("unchecked")
635
                        List<Object> values = (List<Object>)value;
636
                        for (Object obj : values) {
637
                            GraphObject graphObject = GraphObject.Factory.create();
638
                            graphObject.setProperty(propertyName, obj);
639
                            graphObjects.add(graphObject);
640
                        }
641
 
642
                        value = graphObjects;
643
                    } else {
644
                        GraphObject graphObject = GraphObject.Factory.create();
645
                        graphObject.setProperty(propertyName, value);
646
 
647
                        value = graphObject;
648
                    }
649
                }
650
 
651
                return value;
652
            }
653
 
654
            private final Object proxyGraphObjectGettersAndSetters(Method method, Object[] args) throws JSONException {
655
                String methodName = method.getName();
656
                int parameterCount = method.getParameterTypes().length;
657
                PropertyName propertyNameOverride = method.getAnnotation(PropertyName.class);
658
 
659
                String key = propertyNameOverride != null ? propertyNameOverride.value() :
660
                        convertCamelCaseToLowercaseWithUnderscores(methodName.substring(3));
661
 
662
                // If it's a get or a set on a GraphObject-derived class, we can handle it.
663
                if (parameterCount == 0) {
664
                    // Has to be a getter. ASSUMPTION: The GraphObject-derived class has been verified
665
                    Object value = this.state.opt(key);
666
 
667
                    Class<?> expectedType = method.getReturnType();
668
 
669
                    Type genericReturnType = method.getGenericReturnType();
670
                    ParameterizedType parameterizedReturnType = null;
671
                    if (genericReturnType instanceof ParameterizedType) {
672
                        parameterizedReturnType = (ParameterizedType) genericReturnType;
673
                    }
674
 
675
                    value = coerceValueToExpectedType(value, expectedType, parameterizedReturnType);
676
 
677
                    return value;
678
                } else if (parameterCount == 1) {
679
                    // Has to be a setter. ASSUMPTION: The GraphObject-derived class has been verified
680
                    CreateGraphObject createGraphObjectAnnotation = method.getAnnotation(CreateGraphObject.class);
681
                    Object value = createGraphObjectsFromParameters(createGraphObjectAnnotation, args[0]);
682
 
683
                    // If this is a wrapped object, store the underlying JSONObject instead, in order to serialize
684
                    // correctly.
685
                    value = getUnderlyingJSONObject(value);
686
                    this.state.putOpt(key, value);
687
                    return null;
688
                }
689
 
690
                return throwUnexpectedMethodSignature(method);
691
            }
692
 
693
            private Object setJSONProperty(Object[] args) {
694
                String name = (String) args[0];
695
                Object property = args[1];
696
                Object value = getUnderlyingJSONObject(property);
697
                try {
698
                    state.putOpt(name, value);
699
                } catch (JSONException e) {
700
                    throw new IllegalArgumentException(e);
701
                }
702
                return null;
703
            }
704
        }
705
 
706
        private final static class GraphObjectListImpl<T> extends AbstractList<T> implements GraphObjectList<T> {
707
            private final JSONArray state;
708
            private final Class<?> itemType;
709
 
710
            public GraphObjectListImpl(JSONArray state, Class<?> itemType) {
711
                Validate.notNull(state, "state");
712
                Validate.notNull(itemType, "itemType");
713
 
714
                this.state = state;
715
                this.itemType = itemType;
716
            }
717
 
718
            @Override
719
            public String toString() {
720
                return String.format("GraphObjectList{itemType=%s, state=%s}", itemType.getSimpleName(), state);
721
            }
722
 
723
            @Override
724
            public void add(int location, T object) {
725
                // We only support adding at the end of the list, due to JSONArray restrictions.
726
                if (location < 0) {
727
                    throw new IndexOutOfBoundsException();
728
                } else if (location < size()) {
729
                    throw new UnsupportedOperationException("Only adding items at the end of the list is supported.");
730
                }
731
 
732
                put(location, object);
733
            }
734
 
735
            @Override
736
            public T set(int location, T object) {
737
                checkIndex(location);
738
 
739
                T result = get(location);
740
                put(location, object);
741
                return result;
742
            }
743
 
744
            @Override
745
            public int hashCode() {
746
                return state.hashCode();
747
            }
748
 
749
            @Override
750
            public boolean equals(Object obj) {
751
                if (obj == null) {
752
                    return false;
753
                } else if (this == obj) {
754
                    return true;
755
                } else if (getClass() != obj.getClass()) {
756
                    return false;
757
                }
758
                @SuppressWarnings("unchecked")
759
                GraphObjectListImpl<T> other = (GraphObjectListImpl<T>) obj;
760
                return state.equals(other.state);
761
            }
762
 
763
            @SuppressWarnings("unchecked")
764
            @Override
765
            public T get(int location) {
766
                checkIndex(location);
767
 
768
                Object value = state.opt(location);
769
 
770
                // Class<?> expectedType = method.getReturnType();
771
                // Type genericType = method.getGenericReturnType();
772
                T result = (T) coerceValueToExpectedType(value, itemType, null);
773
 
774
                return result;
775
            }
776
 
777
            @Override
778
            public int size() {
779
                return state.length();
780
            }
781
 
782
            @Override
783
            public final <U extends GraphObject> GraphObjectList<U> castToListOf(Class<U> graphObjectClass) {
784
                if (GraphObject.class.isAssignableFrom(itemType)) {
785
                    if (graphObjectClass.isAssignableFrom(itemType)) {
786
                        @SuppressWarnings("unchecked")
787
                        GraphObjectList<U> result = (GraphObjectList<U>)this;
788
                        return result;
789
                    }
790
 
791
                    return createList(state, graphObjectClass);
792
                } else {
793
                    throw new FacebookGraphObjectException("Can't cast GraphObjectCollection of non-GraphObject type "
794
                            + itemType);
795
                }
796
            }
797
 
798
            @Override
799
            public final JSONArray getInnerJSONArray() {
800
                return state;
801
            }
802
 
803
            @Override
804
            public void clear() {
805
                throw new UnsupportedOperationException();
806
            }
807
 
808
            @Override
809
            public boolean remove(Object o) {
810
                throw new UnsupportedOperationException();
811
            }
812
 
813
            @Override
814
            public boolean removeAll(Collection<?> c) {
815
                throw new UnsupportedOperationException();
816
            }
817
 
818
            @Override
819
            public boolean retainAll(Collection<?> c) {
820
                throw new UnsupportedOperationException();
821
            }
822
 
823
            private void checkIndex(int index) {
824
                if (index < 0 || index >= state.length()) {
825
                    throw new IndexOutOfBoundsException();
826
                }
827
            }
828
 
829
            private void put(int index, T obj) {
830
                Object underlyingObject = getUnderlyingJSONObject(obj);
831
                try {
832
                    state.put(index, underlyingObject);
833
                } catch (JSONException e) {
834
                    throw new IllegalArgumentException(e);
835
                }
836
            }
837
        }
838
    }
839
}