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  
19  package org.apache.shindig.social.sample.spi;
20  
21  import org.apache.shindig.auth.SecurityToken;
22  import org.apache.shindig.common.util.ImmediateFuture;
23  import org.apache.shindig.common.util.ResourceLoader;
24  import org.apache.shindig.social.ResponseError;
25  import org.apache.shindig.social.opensocial.model.Activity;
26  import org.apache.shindig.social.opensocial.model.Person;
27  import org.apache.shindig.social.opensocial.service.BeanConverter;
28  import org.apache.shindig.social.opensocial.spi.ActivityService;
29  import org.apache.shindig.social.opensocial.spi.AppDataService;
30  import org.apache.shindig.social.opensocial.spi.CollectionOptions;
31  import org.apache.shindig.social.opensocial.spi.DataCollection;
32  import org.apache.shindig.social.opensocial.spi.GroupId;
33  import org.apache.shindig.social.opensocial.spi.PersonService;
34  import org.apache.shindig.social.opensocial.spi.RestfulCollection;
35  import org.apache.shindig.social.opensocial.spi.SocialSpiException;
36  import org.apache.shindig.social.opensocial.spi.UserId;
37  
38  import com.google.common.collect.ImmutableSortedSet;
39  import com.google.common.collect.Lists;
40  import com.google.common.collect.Maps;
41  import com.google.common.collect.Sets;
42  import com.google.inject.Inject;
43  import com.google.inject.Singleton;
44  import com.google.inject.name.Named;
45  
46  import org.apache.commons.io.IOUtils;
47  import org.json.JSONArray;
48  import org.json.JSONException;
49  import org.json.JSONObject;
50  
51  import java.util.Collections;
52  import java.util.Comparator;
53  import java.util.Iterator;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.Set;
57  import java.util.concurrent.Future;
58  
59  /***
60   * Implementation of supported services backed by a JSON DB.
61   */
62  @Singleton
63  public class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService {
64  
65    private static final Comparator<Person> NAME_COMPARATOR = new Comparator<Person>() {
66      public int compare(Person person, Person person1) {
67        String name = person.getName().getUnstructured();
68        String name1 = person1.getName().getUnstructured();
69        return name.compareTo(name1);
70      }
71    };
72  
73    /***
74     * The DB
75     */
76    private JSONObject db;
77  
78    /***
79     * The JSON<->Bean converter
80     */
81    private BeanConverter converter;
82  
83    /***
84     * db["activities"] -> Array<Person>
85     */
86    private static final String PEOPLE_TABLE = "people";
87  
88    /***
89     * db["people"] -> Map<Person.Id, Array<Activity>>
90     */
91    private static final String ACTIVITIES_TABLE = "activities";
92  
93    /***
94     * db["data"] -> Map<Person.Id, Map<String, String>>
95     */
96    private static final String DATA_TABLE = "data";
97  
98    /***
99     * db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
100    */
101   private static final String FRIEND_LINK_TABLE = "friendLinks";
102 
103   @Inject
104   public JsonDbOpensocialService(@Named("shindig.canonical.json.db")String jsonLocation,
105       @Named("shindig.bean.converter.json")BeanConverter converter) throws Exception {
106     String content = IOUtils.toString(ResourceLoader.openResource(jsonLocation), "UTF-8");
107     this.db = new JSONObject(content);
108     this.converter = converter;
109   }
110 
111   public JSONObject getDb() {
112     return db;
113   }
114 
115   public void setDb(JSONObject db) {
116     this.db = db;
117   }
118 
119   public Future<RestfulCollection<Activity>> getActivities(Set<UserId> userIds,
120       GroupId groupId, String appId, Set<String> fields, SecurityToken token)
121       throws SocialSpiException  {
122     List<Activity> result = Lists.newArrayList();
123     try {
124       Set<String> idSet = getIdSet(userIds, groupId, token);
125       for (String id : idSet) {
126         if (db.getJSONObject(ACTIVITIES_TABLE).has(id)) {
127           JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(id);
128           for (int i = 0; i < activities.length(); i++) {
129             JSONObject activity = activities.getJSONObject(i);
130             if (appId == null || !activity.has(Activity.Field.APP_ID.toString())) {
131               result.add(convertToActivity(activity, fields));
132             } else if (activity.get(Activity.Field.APP_ID.toString()).equals(appId)) {
133               result.add(convertToActivity(activity, fields));
134             }
135           }
136         }
137       }
138       return ImmediateFuture.newInstance(new RestfulCollection<Activity>(result));
139     } catch (JSONException je) {
140       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
141     }
142   }
143 
144   public Future<RestfulCollection<Activity>> getActivities(UserId userId,
145       GroupId groupId, String appId, Set<String> fields,
146       Set<String> activityIds, SecurityToken token) throws SocialSpiException {
147     List<Activity> result = Lists.newArrayList();
148     try {
149       String user = userId.getUserId(token);
150       if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
151         JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
152         for (int i = 0; i < activities.length(); i++) {
153           JSONObject activity = activities.getJSONObject(i);
154           if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
155               && activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
156             result.add(convertToActivity(activity, fields));
157           }
158         }
159       }
160       return ImmediateFuture.newInstance(new RestfulCollection<Activity>(result));
161     } catch (JSONException je) {
162       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
163     }
164   }
165 
166   public Future<Activity> getActivity(UserId userId,
167       GroupId groupId, String appId, Set<String> fields, String activityId, SecurityToken token)
168       throws SocialSpiException {
169     try {
170       String user = userId.getUserId(token);
171       if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
172         JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
173         for (int i = 0; i < activities.length(); i++) {
174           JSONObject activity = activities.getJSONObject(i);
175           if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
176               && activity.get(Activity.Field.ID.toString()).equals(activityId)) {
177             return ImmediateFuture.newInstance(convertToActivity(activity, fields));
178           }
179         }
180       }
181 
182       throw new SocialSpiException(ResponseError.BAD_REQUEST, "Activity not found");
183     } catch (JSONException je) {
184       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
185     }
186   }
187 
188   public Future<Void> deleteActivities(UserId userId, GroupId groupId, String appId,
189       Set<String> activityIds, SecurityToken token) throws SocialSpiException {
190     try {
191       String user = userId.getUserId(token);
192       if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
193         JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
194         if (activities != null) {
195           JSONArray newList = new JSONArray();
196           for (int i = 0; i < activities.length(); i++) {
197             JSONObject activity = activities.getJSONObject(i);
198             if (!activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
199               newList.put(activity);
200             }
201           }
202           db.getJSONObject(ACTIVITIES_TABLE).put(user, newList);
203           // TODO. This seems very odd that we return no useful response in this case
204           // There is no way to represent not-found
205           // if (found) { ??
206           //}
207         }
208       }
209       // What is the appropriate response here??
210       return ImmediateFuture.newInstance(null);
211     } catch (JSONException je) {
212       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
213     }
214   }
215 
216   public Future<Void> createActivity(UserId userId, GroupId groupId, String appId,
217       Set<String> fields, Activity activity, SecurityToken token) throws SocialSpiException {
218     // Are fields really needed here?
219     try {
220       JSONObject jsonObject = convertFromActivity(activity, fields);
221       if (!jsonObject.has(Activity.Field.ID.toString())) {
222         jsonObject.put(Activity.Field.ID.toString(), System.currentTimeMillis());
223       }
224       JSONArray jsonArray = db.getJSONObject(ACTIVITIES_TABLE)
225           .getJSONArray(userId.getUserId(token));
226       if (jsonArray == null) {
227         jsonArray = new JSONArray();
228         db.getJSONObject(ACTIVITIES_TABLE).put(userId.getUserId(token), jsonArray);
229       }
230       jsonArray.put(jsonObject);
231       return ImmediateFuture.newInstance(null);
232     } catch (JSONException je) {
233       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
234     }
235   }
236 
237   public Future<RestfulCollection<Person>> getPeople(Set<UserId> userIds,
238       GroupId groupId, CollectionOptions options, Set<String> fields, SecurityToken token)
239       throws SocialSpiException {
240     List<Person> result = Lists.newArrayList();
241     try {
242       JSONArray people = db.getJSONArray(PEOPLE_TABLE);
243 
244       Set<String> idSet = getIdSet(userIds, groupId, token);
245 
246       for (int i = 0; i < people.length(); i++) {
247         JSONObject person = people.getJSONObject(i);
248         if (!idSet.contains(person.get(Person.Field.ID.toString()))) {
249           continue;
250         }
251         // Add group support later
252         result.add(convertToPerson(person, fields));
253       }
254 
255       // We can pretend that by default the people are in top friends order
256       if (options.getSortBy().equals(Person.Field.NAME.toString())) {
257         Collections.sort(result, NAME_COMPARATOR);
258       }
259 
260       if (options.getSortOrder().equals(SortOrder.descending)) {
261         Collections.reverse(result);
262       }
263 
264       // TODO: The samplecontainer doesn't really have the concept of HAS_APP so
265       // we can't support any filters yet. We should fix this.
266 
267       int totalSize = result.size();
268       int last = options.getFirst() + options.getMax();
269       result = result.subList(options.getFirst(), Math.min(last, totalSize));
270 
271       return ImmediateFuture.newInstance(new RestfulCollection<Person>(
272           result, options.getFirst(), totalSize));
273     } catch (JSONException je) {
274       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
275     }
276   }
277 
278   public Future<Person> getPerson(UserId id, Set<String> fields,
279       SecurityToken token) throws SocialSpiException {
280     try {
281       JSONArray people = db.getJSONArray(PEOPLE_TABLE);
282 
283       for (int i = 0; i < people.length(); i++) {
284         JSONObject person = people.getJSONObject(i);
285         if (id != null && person.get(Person.Field.ID.toString())
286             .equals(id.getUserId(token))) {
287           return ImmediateFuture.newInstance(convertToPerson(person, fields));
288         }
289       }
290       throw new SocialSpiException(ResponseError.BAD_REQUEST, "Person not found");
291     } catch (JSONException je) {
292       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
293     }
294   }
295 
296   public Future<DataCollection> getPersonData(Set<UserId> userIds, GroupId groupId,
297       String appId, Set<String> fields, SecurityToken token) throws SocialSpiException {
298     try {
299       Map<String, Map<String, String>> idToData = Maps.newHashMap();
300       Set<String> idSet = getIdSet(userIds, groupId, token);
301       for (String id : idSet) {
302         JSONObject personData;
303         if (!db.getJSONObject(DATA_TABLE).has(id)) {
304           personData = new JSONObject();
305         } else {
306           if (!fields.isEmpty()) {
307             personData = new JSONObject(
308                 db.getJSONObject(DATA_TABLE).getJSONObject(id),
309                 fields.toArray(new String[fields.size()]));
310           } else {
311             personData = db.getJSONObject(DATA_TABLE).getJSONObject(id);
312           }
313         }
314 
315         // TODO: We can use the converter here to do this for us
316         Iterator keys = personData.keys();
317         Map<String, String> data = Maps.newHashMap();
318         while (keys.hasNext()) {
319           String key = (String) keys.next();
320           data.put(key, personData.getString(key));
321         }
322         idToData.put(id, data);
323       }
324       return ImmediateFuture.newInstance(new DataCollection(idToData));
325     } catch (JSONException je) {
326       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
327     }
328   }
329 
330   public Future<Void> deletePersonData(UserId userId, GroupId groupId, String appId,
331       Set<String> fields, SecurityToken token) throws SocialSpiException {
332     try {
333       String user = userId.getUserId(token);
334       if (!db.getJSONObject(DATA_TABLE).has(user)) {
335         return null;
336       }
337       JSONObject newPersonData = new JSONObject();
338       JSONObject oldPersonData = db.getJSONObject(DATA_TABLE).getJSONObject(user);
339       Iterator keys = oldPersonData.keys();
340       while (keys.hasNext()) {
341         String key = (String) keys.next();
342         if (!fields.contains(key)) {
343           newPersonData.put(key, oldPersonData.getString(key));
344         }
345       }
346       db.getJSONObject(DATA_TABLE).put(user, newPersonData);
347       return ImmediateFuture.newInstance(null);
348     } catch (JSONException je) {
349       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
350     }
351   }
352 
353   public Future<Void> updatePersonData(UserId userId, GroupId groupId, String appId,
354       Set<String> fields, Map<String, String> values, SecurityToken token)
355       throws SocialSpiException {
356     // TODO: this seems redundant. No need to pass both fields and a map of field->value
357     // TODO: According to rest, yes there is. If a field is in the param list but not in the map
358     // that means it is a delete
359 
360     try {
361       JSONObject personData = db.getJSONObject(DATA_TABLE).getJSONObject(userId.getUserId(token));
362       if (personData == null) {
363         personData = new JSONObject();
364         db.getJSONObject(DATA_TABLE).put(userId.getUserId(token), personData);
365       }
366 
367       for (Map.Entry<String, String> entry : values.entrySet()) {
368         personData.put(entry.getKey(), entry.getValue());
369       }
370       return ImmediateFuture.newInstance(null);
371     } catch (JSONException je) {
372       throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
373     }
374   }
375 
376   /***
377    * Get the set of user id's from a user and group
378    */
379   private Set<String> getIdSet(UserId user, GroupId group, SecurityToken token)
380       throws JSONException {
381     String userId = user.getUserId(token);
382 
383     if (group == null) {
384       return ImmutableSortedSet.of(userId);
385     }
386 
387     Set<String> returnVal = Sets.newLinkedHashSet();
388     switch (group.getType()) {
389       case all:
390       case friends:
391       case groupId:
392         if (db.getJSONObject(FRIEND_LINK_TABLE).has(userId)) {
393           JSONArray friends = db.getJSONObject(FRIEND_LINK_TABLE).getJSONArray(userId);
394           for (int i = 0; i < friends.length(); i++) {
395             returnVal.add(friends.getString(i));
396           }
397         }
398         break;
399       case self:
400         returnVal.add(userId);
401         break;
402     }
403     return returnVal;
404   }
405 
406   /***
407    * Get the set of user id's for a set of users and a group
408    */
409   private Set<String> getIdSet(Set<UserId> users, GroupId group, SecurityToken token)
410       throws JSONException {
411     Set<String> ids = Sets.newLinkedHashSet();
412     for (UserId user : users) {
413       ids.addAll(getIdSet(user, group, token));
414     }
415     return ids;
416   }
417 
418   private Activity convertToActivity(JSONObject object, Set<String> fields) throws JSONException {
419     if (!fields.isEmpty()) {
420       // Create a copy with just the specified fields
421       object = new JSONObject(object, fields.toArray(new String[fields.size()]));
422     }
423     return converter.convertToObject(object.toString(), Activity.class);
424   }
425 
426   private JSONObject convertFromActivity(Activity activity, Set<String> fields)
427       throws JSONException {
428     // TODO Not using fields yet
429     return new JSONObject(converter.convertToString(activity));
430   }
431 
432   private Person convertToPerson(JSONObject object, Set<String> fields) throws JSONException {
433     if (!fields.isEmpty()) {
434       // Create a copy with just the specified fields
435       object = new JSONObject(object, fields.toArray(new String[fields.size()]));
436     }
437     return converter.convertToObject(object.toString(), Person.class);
438   }
439 }