summaryrefslogtreecommitdiffstats
path: root/src/com/vaadin/terminal/gwt/server/JsonCodec.java
blob: 1824a16fb239c4643fc6ac5fac03b39d3dc3601f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/*
@VaadinApache2LicenseForJavaFiles@
 */

package com.vaadin.terminal.gwt.server;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.vaadin.Application;
import com.vaadin.external.json.JSONArray;
import com.vaadin.external.json.JSONException;
import com.vaadin.external.json.JSONObject;
import com.vaadin.terminal.gwt.client.Connector;
import com.vaadin.terminal.gwt.client.communication.JsonEncoder;

/**
 * Decoder for converting RPC parameters and other values from JSON in transfer
 * between the client and the server and vice versa.
 * 
 * @since 7.0
 */
public class JsonCodec implements Serializable {

    private static Map<Class<?>, String> typeToTransportType = new HashMap<Class<?>, String>();

    static {
        registerType(String.class, JsonEncoder.VTYPE_STRING);
        registerType(Connector.class, JsonEncoder.VTYPE_CONNECTOR);
        registerType(Boolean.class, JsonEncoder.VTYPE_BOOLEAN);
        registerType(boolean.class, JsonEncoder.VTYPE_BOOLEAN);
        registerType(Integer.class, JsonEncoder.VTYPE_INTEGER);
        registerType(int.class, JsonEncoder.VTYPE_INTEGER);
        registerType(Float.class, JsonEncoder.VTYPE_FLOAT);
        registerType(float.class, JsonEncoder.VTYPE_FLOAT);
        registerType(Double.class, JsonEncoder.VTYPE_DOUBLE);
        registerType(double.class, JsonEncoder.VTYPE_DOUBLE);
        registerType(Long.class, JsonEncoder.VTYPE_LONG);
        registerType(long.class, JsonEncoder.VTYPE_LONG);
        // transported as string representation
        registerType(Enum.class, JsonEncoder.VTYPE_STRING);
        registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY);
        registerType(Object[].class, JsonEncoder.VTYPE_ARRAY);
        registerType(Map.class, JsonEncoder.VTYPE_MAP);
        registerType(List.class, JsonEncoder.VTYPE_LIST);
        registerType(Set.class, JsonEncoder.VTYPE_SET);
    }

    private static void registerType(Class<?> type, String transportType) {
        typeToTransportType.put(type, transportType);
    }

    /**
     * Convert a JSON array with two elements (type and value) into a
     * server-side type, recursively if necessary.
     * 
     * @param value
     *            JSON array with two elements
     * @param application
     *            mapper between connector ID and {@link Connector} objects
     * @return converted value (does not contain JSON types)
     * @throws JSONException
     *             if the conversion fails
     */
    public static Object decode(JSONArray value, Application application)
            throws JSONException {
        return decodeVariableValue(value.getString(0), value.get(1),
                application);
    }

    private static Object decodeVariableValue(String variableType,
            Object value, Application application) throws JSONException {
        Object val = null;
        // TODO type checks etc.
        if (JsonEncoder.VTYPE_ARRAY.equals(variableType)) {
            val = decodeArray((JSONArray) value, application);
        } else if (JsonEncoder.VTYPE_LIST.equals(variableType)) {
            val = decodeList((JSONArray) value, application);
        } else if (JsonEncoder.VTYPE_SET.equals(variableType)) {
            val = decodeSet((JSONArray) value, application);
        } else if (JsonEncoder.VTYPE_MAP_CONNECTOR.equals(variableType)) {
            val = decodeConnectorMap((JSONObject) value, application);
        } else if (JsonEncoder.VTYPE_MAP.equals(variableType)) {
            val = decodeMap((JSONObject) value, application);
        } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(variableType)) {
            val = decodeStringArray((JSONArray) value);
        } else if (JsonEncoder.VTYPE_STRING.equals(variableType)) {
            val = value;
        } else if (JsonEncoder.VTYPE_INTEGER.equals(variableType)) {
            // TODO handle properly
            val = Integer.valueOf(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_LONG.equals(variableType)) {
            // TODO handle properly
            val = Long.valueOf(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_FLOAT.equals(variableType)) {
            // TODO handle properly
            val = Float.valueOf(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_DOUBLE.equals(variableType)) {
            // TODO handle properly
            val = Double.valueOf(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_BOOLEAN.equals(variableType)) {
            // TODO handle properly
            val = Boolean.valueOf(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_CONNECTOR.equals(variableType)) {
            val = application.getConnector(String.valueOf(value));
        } else if (JsonEncoder.VTYPE_NULL.equals(variableType)) {
            val = null;
        } else {
            // Try to decode object using fields
            return decodeObject(variableType, (JSONObject) value, application);

        }

        return val;
    }

    private static Object decodeMap(JSONObject jsonMap, Application application)
            throws JSONException {
        HashMap<String, Object> map = new HashMap<String, Object>();
        Iterator<String> it = jsonMap.keys();
        while (it.hasNext()) {
            String key = it.next();
            map.put(key, decode(jsonMap.getJSONArray(key), application));
        }
        return map;
    }

    private static Object decodeConnectorMap(JSONObject jsonMap,
            Application application) throws JSONException {
        HashMap<Connector, Object> map = new HashMap<Connector, Object>();
        Iterator<String> it = jsonMap.keys();
        while (it.hasNext()) {
            String connectorId = it.next();
            Connector connector = application.getConnector(connectorId);
            map.put(connector,
                    decode(jsonMap.getJSONArray(connectorId), application));
        }
        return map;
    }

    private static String[] decodeStringArray(JSONArray jsonArray)
            throws JSONException {
        int length = jsonArray.length();
        List<String> tokens = new ArrayList<String>(length);
        for (int i = 0; i < length; ++i) {
            tokens.add(jsonArray.getString(i));
        }
        return tokens.toArray(new String[tokens.size()]);
    }

    private static Object decodeArray(JSONArray jsonArray,
            Application application) throws JSONException {
        List list = decodeList(jsonArray, application);
        return list.toArray(new Object[list.size()]);
    }

    private static List<Object> decodeList(JSONArray jsonArray,
            Application application) throws JSONException {
        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < jsonArray.length(); ++i) {
            // each entry always has two elements: type and value
            JSONArray entryArray = jsonArray.getJSONArray(i);
            list.add(decode(entryArray, application));
        }
        return list;
    }

    private static Set<Object> decodeSet(JSONArray jsonArray,
            Application application) throws JSONException {
        HashSet<Object> set = new HashSet<Object>();
        set.addAll(decodeList(jsonArray, application));
        return set;
    }

    /**
     * Encode a value to a JSON representation for transport from the server to
     * the client.
     * 
     * @param value
     *            value to convert
     * @param application
     *            mapper between connector ID and {@link Connector} objects
     * @return JSON representation of the value
     * @throws JSONException
     *             if encoding a value fails (e.g. NaN or infinite number)
     */
    public static JSONArray encode(Object value, Application application)
            throws JSONException {
        return encode(value, null, application);
    }

    public static JSONArray encode(Object value, Class<?> valueType,
            Application application) throws JSONException {

        if (null == value) {
            return combineTypeAndValue(JsonEncoder.VTYPE_NULL, JSONObject.NULL);
        }

        if (valueType == null) {
            valueType = value.getClass();
        }

        String transportType = getTransportType(valueType);
        if (value instanceof String[]) {
            String[] array = (String[]) value;
            JSONArray jsonArray = new JSONArray();
            for (int i = 0; i < array.length; ++i) {
                jsonArray.put(array[i]);
            }
            return combineTypeAndValue(JsonEncoder.VTYPE_STRINGARRAY, jsonArray);
        } else if (value instanceof String) {
            return combineTypeAndValue(JsonEncoder.VTYPE_STRING, value);
        } else if (value instanceof Boolean) {
            return combineTypeAndValue(JsonEncoder.VTYPE_BOOLEAN, value);
        } else if (value instanceof Number) {
            return combineTypeAndValue(transportType, value);
        } else if (value instanceof Collection) {
            if (transportType == null) {
                throw new RuntimeException(
                        "Unable to serialize unsupported type: " + valueType);
            }
            Collection<?> collection = (Collection<?>) value;
            JSONArray jsonArray = encodeCollection(collection, application);

            return combineTypeAndValue(transportType, jsonArray);
        } else if (value instanceof Object[]) {
            Object[] array = (Object[]) value;
            JSONArray jsonArray = encodeArrayContents(array, application);
            return combineTypeAndValue(JsonEncoder.VTYPE_ARRAY, jsonArray);
        } else if (value instanceof Map) {
            Map<Object, Object> map = (Map<Object, Object>) value;
            JSONObject jsonMap = encodeMapContents(map, application);
            // Hack to support Connector as map key. Should be fixed by #
            if (!map.isEmpty()
                    && map.keySet().iterator().next() instanceof Connector) {
                return combineTypeAndValue(JsonEncoder.VTYPE_MAP_CONNECTOR,
                        jsonMap);
            } else {
                return combineTypeAndValue(JsonEncoder.VTYPE_MAP, jsonMap);
            }
        } else if (value instanceof Connector) {
            Connector connector = (Connector) value;
            return combineTypeAndValue(JsonEncoder.VTYPE_CONNECTOR,
                    connector.getConnectorId());
        } else if (transportType != null) {
            return combineTypeAndValue(transportType, String.valueOf(value));
        } else {
            // Any object that we do not know how to encode we encode by looping
            // through fields
            return combineTypeAndValue(valueType.getName(),
                    encodeObject(value, application));
        }
    }

    private static Object encodeObject(Object value, Application application)
            throws JSONException {
        JSONObject jsonMap = new JSONObject();

        try {
            for (PropertyDescriptor pd : Introspector.getBeanInfo(
                    value.getClass()).getPropertyDescriptors()) {
                Class<?> fieldType = pd.getPropertyType();
                String fieldName = getTransportFieldName(pd);
                if (fieldName == null) {
                    continue;
                }
                Method getterMethod = pd.getReadMethod();
                Object fieldValue = getterMethod.invoke(value, (Object[]) null);
                jsonMap.put(fieldName,
                        encode(fieldValue, fieldType, application));
            }
        } catch (Exception e) {
            // TODO: Should exceptions be handled in a different way?
            throw new JSONException(e);
        }
        return jsonMap;
    }

    /**
     * Returns the name that should be used as field name in the JSON. We strip
     * "set" from the setter, keeping the result - this is easy to do on both
     * server and client, avoiding some issues with cASE. E.g setZIndex()
     * becomes "ZIndex". Also ensures that both getter and setter are present,
     * returning null otherwise.
     * 
     * @param pd
     * @return the name to be used or null if both getter and setter are not
     *         found.
     */
    private static String getTransportFieldName(PropertyDescriptor pd) {
        if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
            return null;
        }
        return pd.getWriteMethod().getName().substring(3);
    }

    private static Object decodeObject(String type,
            JSONObject serializedObject, Application application)
            throws JSONException {

        Class<?> cls;
        try {
            cls = Class.forName(type);

            Object decodedObject = cls.newInstance();
            for (PropertyDescriptor pd : Introspector.getBeanInfo(cls)
                    .getPropertyDescriptors()) {

                String fieldName = getTransportFieldName(pd);
                if (fieldName == null) {
                    continue;
                }
                JSONArray encodedObject = serializedObject
                        .getJSONArray(fieldName);
                pd.getWriteMethod().invoke(decodedObject,
                        decode(encodedObject, application));
            }

            return decodedObject;
        } catch (ClassNotFoundException e) {
            throw new JSONException(e);
        } catch (IllegalArgumentException e) {
            throw new JSONException(e);
        } catch (IllegalAccessException e) {
            throw new JSONException(e);
        } catch (InvocationTargetException e) {
            throw new JSONException(e);
        } catch (InstantiationException e) {
            throw new JSONException(e);
        } catch (IntrospectionException e) {
            throw new JSONException(e);
        }
    }

    private static JSONArray encodeArrayContents(Object[] array,
            Application application) throws JSONException {
        JSONArray jsonArray = new JSONArray();
        for (Object o : array) {
            // TODO handle object graph loops?
            jsonArray.put(encode(o, application));
        }
        return jsonArray;
    }

    private static JSONArray encodeCollection(Collection collection,
            Application application) throws JSONException {
        JSONArray jsonArray = new JSONArray();
        for (Object o : collection) {
            // TODO handle object graph loops?
            jsonArray.put(encode(o, application));
        }
        return jsonArray;
    }

    private static JSONObject encodeMapContents(Map<Object, Object> map,
            Application application) throws JSONException {
        JSONObject jsonMap = new JSONObject();
        for (Object mapKey : map.keySet()) {
            Object mapValue = map.get(mapKey);

            if (mapKey instanceof ClientConnector) {
                mapKey = ((ClientConnector) mapKey).getConnectorId();
            }
            if (!(mapKey instanceof String)) {
                throw new JSONException(
                        "Only maps with String/Connector keys are currently supported (#8602)");
            }

            jsonMap.put((String) mapKey, encode(mapValue, application));
        }
        return jsonMap;
    }

    private static JSONArray combineTypeAndValue(String type, Object value) {
        if (type == null) {
            throw new RuntimeException("Type for value " + value
                    + " cannot be null!");
        }
        JSONArray outerArray = new JSONArray();
        outerArray.put(type);
        outerArray.put(value);
        return outerArray;
    }

    /**
     * Gets the transport type for the value. Returns null if no transport type
     * can be found.
     * 
     * @param value
     * @return
     * @throws JSONException
     */
    private static String getTransportType(Object value) {
        if (null == value) {
            return JsonEncoder.VTYPE_NULL;
        }
        return getTransportType(value.getClass());
    }

    /**
     * Gets the transport type for the given class. Returns null if no transport
     * type can be found.
     * 
     * @param valueType
     *            The type that should be transported
     * @return
     * @throws JSONException
     */
    private static String getTransportType(Class<?> valueType) {
        return typeToTransportType.get(valueType);

    }

}