/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.apache.shindig.social.sample.spi;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.util.ImmediateFuture;
import org.apache.shindig.common.util.ResourceLoader;
import org.apache.shindig.social.ResponseError;
import org.apache.shindig.social.opensocial.model.Activity;
import org.apache.shindig.social.opensocial.model.Person;
import org.apache.shindig.social.opensocial.service.BeanConverter;
import org.apache.shindig.social.opensocial.spi.ActivityService;
import org.apache.shindig.social.opensocial.spi.AppDataService;
import org.apache.shindig.social.opensocial.spi.CollectionOptions;
import org.apache.shindig.social.opensocial.spi.DataCollection;
import org.apache.shindig.social.opensocial.spi.GroupId;
import org.apache.shindig.social.opensocial.spi.PersonService;
import org.apache.shindig.social.opensocial.spi.RestfulCollection;
import org.apache.shindig.social.opensocial.spi.SocialSpiException;
import org.apache.shindig.social.opensocial.spi.UserId;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
/**
* Implementation of supported services backed by a JSON DB.
*/
@Singleton
public class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService {
private static final Comparator<Person> NAME_COMPARATOR = new Comparator<Person>() {
public int compare(Person person, Person person1) {
String name = person.getName().getFormatted();
String name1 = person1.getName().getFormatted();
return name.compareTo(name1);
}
};
/**
* The DB
*/
private JSONObject db;
/**
* The JSON<->Bean converter
*/
private BeanConverter converter;
/**
* db["activities"] -> Array<Person>
*/
private static final String PEOPLE_TABLE = "people";
/**
* db["people"] -> Map<Person.Id, Array<Activity>>
*/
private static final String ACTIVITIES_TABLE = "activities";
/**
* db["data"] -> Map<Person.Id, Map<String, String>>
*/
private static final String DATA_TABLE = "data";
/**
* db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
*/
private static final String FRIEND_LINK_TABLE = "friendLinks";
@Inject
public JsonDbOpensocialService(@Named("shindig.canonical.json.db")String jsonLocation,
@Named("shindig.bean.converter.json")BeanConverter converter) throws Exception {
String content = IOUtils.toString(ResourceLoader.openResource(jsonLocation), "UTF-8");
this.db = new JSONObject(content);
this.converter = converter;
}
public JSONObject getDb() {
return db;
}
public void setDb(JSONObject db) {
this.db = db;
}
public Future<RestfulCollection<Activity>> getActivities(Set<UserId> userIds,
GroupId groupId, String appId, Set<String> fields, SecurityToken token)
throws SocialSpiException {
List<Activity> result = Lists.newArrayList();
try {
Set<String> idSet = getIdSet(userIds, groupId, token);
for (String id : idSet) {
if (db.getJSONObject(ACTIVITIES_TABLE).has(id)) {
JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(id);
for (int i = 0; i < activities.length(); i++) {
JSONObject activity = activities.getJSONObject(i);
if (appId == null || !activity.has(Activity.Field.APP_ID.toString())) {
result.add(convertToActivity(activity, fields));
} else if (activity.get(Activity.Field.APP_ID.toString()).equals(appId)) {
result.add(convertToActivity(activity, fields));
}
}
}
}
return ImmediateFuture.newInstance(new RestfulCollection<Activity>(result));
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<RestfulCollection<Activity>> getActivities(UserId userId,
GroupId groupId, String appId, Set<String> fields,
Set<String> activityIds, SecurityToken token) throws SocialSpiException {
List<Activity> result = Lists.newArrayList();
try {
String user = userId.getUserId(token);
if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
for (int i = 0; i < activities.length(); i++) {
JSONObject activity = activities.getJSONObject(i);
if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
&& activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
result.add(convertToActivity(activity, fields));
}
}
}
return ImmediateFuture.newInstance(new RestfulCollection<Activity>(result));
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Activity> getActivity(UserId userId,
GroupId groupId, String appId, Set<String> fields, String activityId, SecurityToken token)
throws SocialSpiException {
try {
String user = userId.getUserId(token);
if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
for (int i = 0; i < activities.length(); i++) {
JSONObject activity = activities.getJSONObject(i);
if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
&& activity.get(Activity.Field.ID.toString()).equals(activityId)) {
return ImmediateFuture.newInstance(convertToActivity(activity, fields));
}
}
}
throw new SocialSpiException(ResponseError.BAD_REQUEST, "Activity not found");
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Void> deleteActivities(UserId userId, GroupId groupId, String appId,
Set<String> activityIds, SecurityToken token) throws SocialSpiException {
try {
String user = userId.getUserId(token);
if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
if (activities != null) {
JSONArray newList = new JSONArray();
for (int i = 0; i < activities.length(); i++) {
JSONObject activity = activities.getJSONObject(i);
if (!activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
newList.put(activity);
}
}
db.getJSONObject(ACTIVITIES_TABLE).put(user, newList);
// TODO. This seems very odd that we return no useful response in this case
// There is no way to represent not-found
// if (found) { ??
//}
}
}
// What is the appropriate response here??
return ImmediateFuture.newInstance(null);
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Void> createActivity(UserId userId, GroupId groupId, String appId,
Set<String> fields, Activity activity, SecurityToken token) throws SocialSpiException {
// Are fields really needed here?
try {
JSONObject jsonObject = convertFromActivity(activity, fields);
if (!jsonObject.has(Activity.Field.ID.toString())) {
jsonObject.put(Activity.Field.ID.toString(), System.currentTimeMillis());
}
JSONArray jsonArray = db.getJSONObject(ACTIVITIES_TABLE)
.getJSONArray(userId.getUserId(token));
if (jsonArray == null) {
jsonArray = new JSONArray();
db.getJSONObject(ACTIVITIES_TABLE).put(userId.getUserId(token), jsonArray);
}
jsonArray.put(jsonObject);
return ImmediateFuture.newInstance(null);
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<RestfulCollection<Person>> getPeople(Set<UserId> userIds,
GroupId groupId, CollectionOptions options, Set<String> fields, SecurityToken token)
throws SocialSpiException {
List<Person> result = Lists.newArrayList();
try {
JSONArray people = db.getJSONArray(PEOPLE_TABLE);
Set<String> idSet = getIdSet(userIds, groupId, token);
for (int i = 0; i < people.length(); i++) {
JSONObject person = people.getJSONObject(i);
if (!idSet.contains(person.get(Person.Field.ID.toString()))) {
continue;
}
// Add group support later
result.add(convertToPerson(person, fields));
}
// We can pretend that by default the people are in top friends order
if (options.getSortBy().equals(Person.Field.NAME.toString())) {
Collections.sort(result, NAME_COMPARATOR);
}
if (options.getSortOrder().equals(SortOrder.descending)) {
Collections.reverse(result);
}
// TODO: The samplecontainer doesn't really have the concept of HAS_APP so
// we can't support any filters yet. We should fix this.
int totalSize = result.size();
int last = options.getFirst() + options.getMax();
result = result.subList(options.getFirst(), Math.min(last, totalSize));
return ImmediateFuture.newInstance(new RestfulCollection<Person>(
result, options.getFirst(), totalSize));
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Person> getPerson(UserId id, Set<String> fields,
SecurityToken token) throws SocialSpiException {
try {
JSONArray people = db.getJSONArray(PEOPLE_TABLE);
for (int i = 0; i < people.length(); i++) {
JSONObject person = people.getJSONObject(i);
if (id != null && person.get(Person.Field.ID.toString())
.equals(id.getUserId(token))) {
return ImmediateFuture.newInstance(convertToPerson(person, fields));
}
}
throw new SocialSpiException(ResponseError.BAD_REQUEST, "Person not found");
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<DataCollection> getPersonData(Set<UserId> userIds, GroupId groupId,
String appId, Set<String> fields, SecurityToken token) throws SocialSpiException {
try {
Map<String, Map<String, String>> idToData = Maps.newHashMap();
Set<String> idSet = getIdSet(userIds, groupId, token);
for (String id : idSet) {
JSONObject personData;
if (!db.getJSONObject(DATA_TABLE).has(id)) {
personData = new JSONObject();
} else {
if (!fields.isEmpty()) {
personData = new JSONObject(
db.getJSONObject(DATA_TABLE).getJSONObject(id),
fields.toArray(new String[fields.size()]));
} else {
personData = db.getJSONObject(DATA_TABLE).getJSONObject(id);
}
}
// TODO: We can use the converter here to do this for us
Iterator keys = personData.keys();
Map<String, String> data = Maps.newHashMap();
while (keys.hasNext()) {
String key = (String) keys.next();
data.put(key, personData.getString(key));
}
idToData.put(id, data);
}
return ImmediateFuture.newInstance(new DataCollection(idToData));
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Void> deletePersonData(UserId userId, GroupId groupId, String appId,
Set<String> fields, SecurityToken token) throws SocialSpiException {
try {
String user = userId.getUserId(token);
if (!db.getJSONObject(DATA_TABLE).has(user)) {
return null;
}
JSONObject newPersonData = new JSONObject();
JSONObject oldPersonData = db.getJSONObject(DATA_TABLE).getJSONObject(user);
Iterator keys = oldPersonData.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
if (!fields.contains(key)) {
newPersonData.put(key, oldPersonData.getString(key));
}
}
db.getJSONObject(DATA_TABLE).put(user, newPersonData);
return ImmediateFuture.newInstance(null);
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
public Future<Void> updatePersonData(UserId userId, GroupId groupId, String appId,
Set<String> fields, Map<String, String> values, SecurityToken token)
throws SocialSpiException {
// TODO: this seems redundant. No need to pass both fields and a map of field->value
// TODO: According to rest, yes there is. If a field is in the param list but not in the map
// that means it is a delete
try {
JSONObject personData = db.getJSONObject(DATA_TABLE).getJSONObject(userId.getUserId(token));
if (personData == null) {
personData = new JSONObject();
db.getJSONObject(DATA_TABLE).put(userId.getUserId(token), personData);
}
for (Map.Entry<String, String> entry : values.entrySet()) {
personData.put(entry.getKey(), entry.getValue());
}
return ImmediateFuture.newInstance(null);
} catch (JSONException je) {
throw new SocialSpiException(ResponseError.INTERNAL_ERROR, je.getMessage(), je);
}
}
/**
* Get the set of user id's from a user and group
*/
private Set<String> getIdSet(UserId user, GroupId group, SecurityToken token)
throws JSONException {
String userId = user.getUserId(token);
if (group == null) {
return ImmutableSortedSet.of(userId);
}
Set<String> returnVal = Sets.newLinkedHashSet();
switch (group.getType()) {
case all:
case friends:
case groupId:
if (db.getJSONObject(FRIEND_LINK_TABLE).has(userId)) {
JSONArray friends = db.getJSONObject(FRIEND_LINK_TABLE).getJSONArray(userId);
for (int i = 0; i < friends.length(); i++) {
returnVal.add(friends.getString(i));
}
}
break;
case self:
returnVal.add(userId);
break;
}
return returnVal;
}
/**
* Get the set of user id's for a set of users and a group
*/
private Set<String> getIdSet(Set<UserId> users, GroupId group, SecurityToken token)
throws JSONException {
Set<String> ids = Sets.newLinkedHashSet();
for (UserId user : users) {
ids.addAll(getIdSet(user, group, token));
}
return ids;
}
private Activity convertToActivity(JSONObject object, Set<String> fields) throws JSONException {
if (!fields.isEmpty()) {
// Create a copy with just the specified fields
object = new JSONObject(object, fields.toArray(new String[fields.size()]));
}
return converter.convertToObject(object.toString(), Activity.class);
}
private JSONObject convertFromActivity(Activity activity, Set<String> fields)
throws JSONException {
// TODO Not using fields yet
return new JSONObject(converter.convertToString(activity));
}
private Person convertToPerson(JSONObject object, Set<String> fields) throws JSONException {
if (!fields.isEmpty()) {
// Create a copy with just the specified fields
object = new JSONObject(object, fields.toArray(new String[fields.size()]));
}
return converter.convertToObject(object.toString(), Person.class);
}
}