View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations under the License.
17   */
18  package org.apache.shindig.social.core.util;
19  
20  import org.apache.shindig.social.core.model.EnumImpl;
21  import org.apache.shindig.social.opensocial.model.Enum;
22  import org.apache.shindig.social.opensocial.service.BeanConverter;
23  
24  import com.google.common.collect.ImmutableSet;
25  import com.google.common.collect.Lists;
26  import com.google.common.collect.Maps;
27  import com.google.common.collect.Sets;
28  import com.google.inject.Inject;
29  import com.google.inject.Injector;
30  
31  import org.joda.time.DateTime;
32  import org.json.JSONArray;
33  import org.json.JSONException;
34  import org.json.JSONObject;
35  
36  import java.lang.reflect.InvocationTargetException;
37  import java.lang.reflect.Method;
38  import java.lang.reflect.ParameterizedType;
39  import java.lang.reflect.Type;
40  import java.util.Date;
41  import java.util.Iterator;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Map.Entry;
45  import java.util.Set;
46  import java.util.concurrent.ConcurrentHashMap;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  /***
51   * Converts pojos to json objects.
52   * TODO: Replace with standard library
53   */
54  public class BeanJsonConverter implements BeanConverter {
55  
56    private static final Object[] EMPTY_OBJECT = {};
57    private static final Set<String> EXCLUDED_FIELDS = ImmutableSet.of("class", "declaringclass");
58    private static final String GETTER_PREFIX  = "get";
59    private static final String SETTER_PREFIX = "set";
60  
61    // Only compute the filtered getters/setters once per-class
62    private static final ConcurrentHashMap<Class,List<MethodPair>> GETTER_METHODS = Maps.newConcurrentHashMap();
63    private static final ConcurrentHashMap<Class,List<MethodPair>> SETTER_METHODS = Maps.newConcurrentHashMap();
64  
65    private Injector injector;
66  
67    @Inject
68    public BeanJsonConverter(Injector injector) {
69      this.injector = injector;
70    }
71  
72    public String getContentType() {
73      return "application/json";
74    }
75  
76    /***
77     * Convert the passed in object to a string.
78     *
79     * @param pojo The object to convert
80     * @return An object whos toString method will return json
81     */
82    public String convertToString(final Object pojo) {
83      return convertToJson(pojo).toString();
84    }
85  
86    /***
87     * Convert the passed in object to a json object.
88     *
89     * @param pojo The object to convert
90     * @return An object whos toString method will return json
91     */
92    public Object convertToJson(final Object pojo) {
93      try {
94        return translateObjectToJson(pojo);
95      } catch (JSONException e) {
96        throw new RuntimeException("Could not translate " + pojo + " to json", e);
97      }
98    }
99  
100   private Object translateObjectToJson(final Object val) throws JSONException {
101     if (val instanceof Object[]) {
102       JSONArray array = new JSONArray();
103       for (Object asd : (Object[]) val) {
104         array.put(translateObjectToJson(asd));
105       }
106       return array;
107 
108     } else if (val instanceof List) {
109       JSONArray list = new JSONArray();
110       for (Object item : (List<?>) val) {
111         list.put(translateObjectToJson(item));
112       }
113       return list;
114 
115     } else if (val instanceof Map) {
116       JSONObject map = new JSONObject();
117       Map<?, ?> originalMap = (Map<?, ?>) val;
118 
119       for (Entry<?, ?> item : originalMap.entrySet()) {
120         map.put(item.getKey().toString(), translateObjectToJson(item.getValue()));
121       }
122       return map;
123 
124     } else if (val != null && val.getClass().isEnum()) {
125       return val.toString();
126     } else if (val instanceof String
127         || val instanceof Boolean
128         || val instanceof Integer
129         || val instanceof Date
130         || val instanceof Long
131         || val instanceof Float
132         || val instanceof JSONObject
133         || val instanceof JSONArray
134         || val == null) {
135       return val;
136     }
137 
138     return convertMethodsToJson(val);
139   }
140 
141   /***
142    * Convert the object to {@link JSONObject} reading Pojo properties
143    *
144    * @param pojo The object to convert
145    * @return A JSONObject representing this pojo
146    */
147   private JSONObject convertMethodsToJson(final Object pojo) {
148     List<MethodPair> availableGetters;
149 
150     availableGetters = GETTER_METHODS.get(pojo.getClass());
151     if (availableGetters == null) {
152       availableGetters = getMatchingMethods(pojo, GETTER_PREFIX);
153       GETTER_METHODS.putIfAbsent(pojo.getClass(), availableGetters);
154     }
155 
156     JSONObject toReturn = new JSONObject();
157     for (MethodPair getter : availableGetters) {
158       try {
159         Object val = getter.method.invoke(pojo, EMPTY_OBJECT);
160         if (val != null) {
161           toReturn.put(getter.fieldName, translateObjectToJson(val));
162         }
163       } catch (JSONException e) {
164         throw new RuntimeException(errorMessage(pojo, getter), e);
165       } catch (IllegalAccessException e) {
166         throw new RuntimeException(errorMessage(pojo, getter), e);
167       } catch (InvocationTargetException e) {
168         throw new RuntimeException(errorMessage(pojo, getter), e);
169       } catch (IllegalArgumentException e) {
170         throw new RuntimeException(errorMessage(pojo, getter), e);
171       }
172     }
173     return toReturn;
174   }
175 
176   private static String errorMessage(Object pojo, MethodPair getter) {
177     return "Could not encode the " + getter.method + " method on "
178         + pojo.getClass().getName();
179   }
180 
181   private static final class MethodPair {
182     public Method method;
183     public String fieldName;
184 
185     private MethodPair(final Method method, final String fieldName) {
186       this.method = method;
187       this.fieldName = fieldName;
188     }
189   }
190 
191 
192   private List<MethodPair> getMatchingMethods(Object pojo, String prefix) {
193 
194     List<MethodPair> availableGetters = Lists.newArrayList();
195     Method[] methods = pojo.getClass().getMethods();
196     for (Method method : methods) {
197       String name = method.getName();
198       if (!method.getName().startsWith(prefix)) {
199         continue;
200       }
201       int prefixlen = prefix.length();
202 
203       String fieldName = name.substring(prefixlen, prefixlen+1).toLowerCase() +
204           name.substring(prefixlen + 1);
205       
206       if (EXCLUDED_FIELDS.contains(fieldName.toLowerCase())) {
207         continue;
208       }
209       availableGetters.add(new MethodPair(method, fieldName));
210     }
211     return availableGetters;
212   }
213 
214   public <T> T convertToObject(String json, Class<T> className) {
215     String errorMessage = "Could not convert " + json + " to " + className;
216 
217     try {
218       T pojo = injector.getInstance(className);
219       return convertToObject(json, pojo);
220     } catch (JSONException e) {
221       throw new RuntimeException(errorMessage, e);
222     } catch (InvocationTargetException e) {
223       throw new RuntimeException(errorMessage, e);
224     } catch (IllegalAccessException e) {
225       throw new RuntimeException(errorMessage, e);
226     } catch (InstantiationException e) {
227       throw new RuntimeException(errorMessage, e);
228     } catch (NoSuchFieldException e) {
229       throw new RuntimeException(errorMessage, e);
230     }
231   }
232 
233   private <T> T convertToObject(String json, T pojo)
234       throws JSONException, InvocationTargetException, IllegalAccessException,
235       InstantiationException, NoSuchFieldException {
236 
237     if (pojo instanceof String) {
238       pojo = (T) json; // This is a weird cast...
239 
240     } else if (pojo instanceof Map) {
241       // TODO: Figure out how to get the actual generic type for the
242       // second Map parameter. Right now we are hardcoding to String
243       Class<?> mapValueClass = String.class;
244 
245       JSONObject jsonObject = new JSONObject(json);
246       Iterator<?> iterator = jsonObject.keys();
247       while (iterator.hasNext()) {
248         String key = (String) iterator.next();
249         Object value = convertToObject(jsonObject.getString(key), mapValueClass);
250         ((Map<String, Object>) pojo).put(key, value);
251       }
252 
253     } else if (pojo instanceof List) {
254       JSONArray array = new JSONArray(json);
255       for (int i = 0; i < array.length(); i++) {
256         ((List<Object>) pojo).add(array.get(i));
257       }
258     } else {
259       JSONObject jsonObject = new JSONObject(json);
260       List<MethodPair> methods;
261       methods = SETTER_METHODS.get(pojo.getClass());
262       if (methods == null) {
263         methods = getMatchingMethods(pojo, SETTER_PREFIX);
264         SETTER_METHODS.putIfAbsent(pojo.getClass(), methods);
265       }
266 
267       for (MethodPair setter : methods) {
268         if (jsonObject.has(setter.fieldName)) {
269           callSetterWithValue(pojo, setter.method, jsonObject, setter.fieldName);
270         }
271       }
272     }
273     return pojo;
274   }
275 
276   private <T> void callSetterWithValue(T pojo, Method method,
277       JSONObject jsonObject, String fieldName)
278       throws IllegalAccessException, InvocationTargetException, NoSuchFieldException,
279       JSONException {
280 
281     Class<?> expectedType = method.getParameterTypes()[0];
282     Object value = null;
283 
284     if (!jsonObject.has(fieldName)) {
285       // Skip
286     } else if (expectedType.equals(List.class)) {
287       ParameterizedType genericListType
288           = (ParameterizedType) method.getGenericParameterTypes()[0];
289       Type type = genericListType.getActualTypeArguments()[0];
290       Class<?> rawType;
291       Class<?> listElementClass;
292       if (type instanceof ParameterizedType) {
293         listElementClass = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
294         rawType = (Class<?>)((ParameterizedType)type).getRawType();
295       } else {
296         listElementClass = (Class<?>) type;
297         rawType = listElementClass;
298       }
299 
300       List<Object> list = Lists.newArrayList();
301       JSONArray jsonArray = jsonObject.getJSONArray(fieldName);
302       for (int i = 0; i < jsonArray.length(); i++) {
303         if (org.apache.shindig.social.opensocial.model.Enum.class
304             .isAssignableFrom(rawType)) {
305           list.add(convertEnum(listElementClass, jsonArray.getJSONObject(i)));
306         } else {
307           list.add(convertToObject(jsonArray.getString(i), listElementClass));
308         }
309       }
310 
311       value = list;
312 
313     } else if (expectedType.equals(Map.class)) {
314       ParameterizedType genericListType
315           = (ParameterizedType) method.getGenericParameterTypes()[0];
316       Type[] types = genericListType.getActualTypeArguments();
317       Class<?> valueClass = (Class<?>) types[1];
318 
319       // We only support keys being typed as Strings.
320       // Nothing else really makes sense in json.
321       Map<String, Object> map = Maps.newHashMap();
322       JSONObject jsonMap = jsonObject.getJSONObject(fieldName);
323 
324       Iterator keys = jsonMap.keys();
325       while (keys.hasNext()) {
326         String keyName = (String) keys.next();
327         map.put(keyName, convertToObject(jsonMap.getString(keyName),
328             valueClass));
329       }
330 
331       value = map;
332 
333     } else if (org.apache.shindig.social.opensocial.model.Enum.class
334         .isAssignableFrom(expectedType)) {
335       // TODO Need to stop using Enum as a class name :(
336       value = convertEnum(
337           (Class<?>)((ParameterizedType) method.getGenericParameterTypes()[0]).
338               getActualTypeArguments()[0],
339           jsonObject.getJSONObject(fieldName));
340     } else if (expectedType.isEnum()) {
341       if (jsonObject.has(fieldName)) {
342         for (Object v : expectedType.getEnumConstants()) {
343           if (v.toString().equals(jsonObject.getString(fieldName))) {
344             value = v;
345             break;
346           }
347         }
348         if (value == null) {
349           throw new IllegalArgumentException(
350               "No enum value  '" + jsonObject.getString(fieldName)
351                   + "' in " + expectedType.getName());
352         }
353       }
354     } else if (expectedType.equals(String.class)) {
355       value = jsonObject.getString(fieldName);
356     } else if (expectedType.equals(Date.class)) {
357       // Use JODA ISO parsing for the conversion
358       value = new DateTime(jsonObject.getString(fieldName)).toDate();
359     } else if (expectedType.equals(Long.class) || expectedType.equals(Long.TYPE)) {
360       value = jsonObject.getLong(fieldName);
361     } else if (expectedType.equals(Integer.class) || expectedType.equals(Integer.TYPE)) {
362       value = jsonObject.getInt(fieldName);
363     } else if (expectedType.equals(Boolean.class) || expectedType.equals(Boolean.TYPE)) {
364       value = jsonObject.getBoolean(fieldName);
365     } else if (expectedType.equals(Float.class) || expectedType.equals(Float.TYPE)) {
366       String stringFloat = jsonObject.getString(fieldName);
367       value = new Float(stringFloat);
368     } else {
369       // Assume its an injected type
370       value = convertToObject(jsonObject.getJSONObject(fieldName).toString(), expectedType);
371     }
372 
373     if (value != null) {
374       method.invoke(pojo, value);
375     }
376   }
377 
378   private Object convertEnum(Class<?> enumKeyType, JSONObject jsonEnum)
379       throws JSONException, IllegalAccessException, NoSuchFieldException {
380     // TODO This isnt injector friendly but perhaps implementors dont need it. If they do a
381     // refactoring of the Enum handling in general is needed.
382     Object value;
383     if (jsonEnum.has(Enum.Field.VALUE.toString())) {
384       Enum.EnumKey enumKey = (Enum.EnumKey) enumKeyType
385           .getField(jsonEnum.getString(Enum.Field.VALUE.toString())).get(null);
386       value = new EnumImpl<Enum.EnumKey>(enumKey,
387           jsonEnum.getString(Enum.Field.DISPLAY_VALUE.toString()));
388     } else {
389       value = new EnumImpl<Enum.EnumKey>(null,
390           jsonEnum.getString(Enum.Field.DISPLAY_VALUE.toString()));
391     }
392     return value;
393   }
394 }