/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.shindig.application; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.List; import java.util.Date; import java.util.concurrent.Future; import javax.servlet.http.HttpServletResponse; import org.springframework.orm.hibernate3.support.HibernateDaoSupport; import org.hibernate.Criteria; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.apache.commons.lang.Validate; import org.apache.shindig.common.util.ImmediateFuture; import org.apache.shindig.auth.SecurityToken; import org.apache.shindig.protocol.ProtocolException; import org.apache.shindig.protocol.RestfulCollection; import org.apache.shindig.protocol.model.FilterOperation; import org.apache.shindig.protocol.model.SortOrder; import org.apache.shindig.social.opensocial.model.Activity; import org.apache.shindig.social.opensocial.spi.ActivityService; import org.apache.shindig.social.opensocial.spi.UserId; import org.apache.shindig.social.opensocial.spi.GroupId; import org.apache.shindig.social.opensocial.spi.CollectionOptions; import com.globant.katari.hibernate.coreuser.domain.CoreUser; import com.globant.katari.shindig.domain.KatariActivity; import com.globant.katari.shindig.domain.Application; import com.globant.katari.shindig.domain.ApplicationRepository; /** An implementation of shindig ActivityService that persists the activities * to a database with hibernate. * * Shindig uses the application url as the appId. */ public class KatariActivityService extends HibernateDaoSupport implements ActivityService { /** The class logger. */ private static Logger log = LoggerFactory.getLogger(KatariActivityService.class); /** The application repository. * * This is never null. */ private final ApplicationRepository applicationRepository; /** The news feed application. * * Gadget id of an application that can read all the activities no matter * which application generated it. This is normally used to implement a news * feed gadget. * * It's optional. If null, all applications will only be able to read their * own activities. */ private String newsFeedApplicationId; /** The filter for the activities, mainly use to implement the Activity's * social graph. * It's optional. If null, will only show activities for group 'self'. */ private KatariActivityFilter katariActivityFilter; /** Constructor, builds a KatariActivityService. * * @param theApplicationRepository the application repository. It cannot be * null. */ public KatariActivityService(final ApplicationRepository theApplicationRepository) { Validate.notNull(theApplicationRepository, "the application repository" + " cannot be null"); applicationRepository = theApplicationRepository; } /** {@inheritDoc} * * TODO: provide a hook to return the other groups: all, friends, groupId and * deleted. * * @param userIds The list of user ids that generated the requested * activities. It cannot be null. * * @param groupId only supports @self, that returns all the activities from * all the provided users. It cannot be null. * * @param appId The application id of activities. It cannot be null. * * @param fields The list of fields to return. It looks like the spec (1.0) * says nothing about this attribute. We ignore it here. * * @param options Options related to the resulting activities, like * filtering, number of returned activities, etc. It returns the * activities in the specified order. It cannot be null. * * @param token the security token of the currently logged on user or * application. It cannot be null. * * @return the found activities, in the specified order. It never returns * null. */ @SuppressWarnings("unchecked") public Future<RestfulCollection<Activity>> getActivities( final Set<UserId> userIds, final GroupId groupId, final String appId, final Set<String> fields, final CollectionOptions options, final SecurityToken token) { log.trace("Entering getActivities"); Validate.notNull(userIds, "The list of user ids cannot be null."); Validate.notNull(groupId, "The group id cannot be null."); Validate.notNull(appId, "The app id cannot be null."); Validate.notNull(options, "The options cannot be null."); Validate.notNull(token, "The security token cannot be null."); Validate.isTrue(options.getMax() > 0, "You should ask for at least one row."); // Workaround: shindig sets this to topFriends if no sort order is // specified. if ("topFriends".equals(options.getSortBy())) { options.setSortBy("id"); options.setSortOrder(SortOrder.descending); } Criteria criteria; criteria = createCriteriaFor(userIds, groupId, appId, options, token); // Obtains the count of activities that matches the search. long totalResults = (Long) criteria.setProjection( Projections.rowCount()).uniqueResult(); // Restore the original projection, removing the count. criteria.setProjection(null); criteria.setResultTransformer(Criteria.ROOT_ENTITY); List<Activity> activities; if (totalResults > 0) { // No need to execute the query if count(*) said 0. addOptionsToCriteria(criteria, options); activities = criteria.list(); } else { activities = new ArrayList<Activity>(); } log.trace("Leaving getActivities"); // Possible loss of precision, but we will hopefully never have more than // 2^31 activities. return ImmediateFuture.newInstance(new RestfulCollection<Activity>( activities, options.getFirst(), (int) totalResults, options.getMax())); } /** {@inheritDoc} * * Returns the activities for a single user. See javadoc for the overloaded * getActivity operation. */ @SuppressWarnings("unchecked") public Future<RestfulCollection<Activity>> getActivities(final UserId userId, final GroupId groupId, final String appId, final Set<String> fields, final CollectionOptions options, final Set<String> activityIds, final SecurityToken token) { log.trace("Entering getActivities"); Validate.notNull(userId, "The user id cannot be null."); Validate.notNull(groupId, "The group id cannot be null."); Validate.notNull(appId, "The app id cannot be null."); Validate.notNull(options, "The options cannot be null."); Validate.notNull(token, "The security token cannot be null."); Validate.isTrue(options.getMax() > 0, "You should ask for at least one row."); Validate.notNull(activityIds, "The list of activity ids cannot be null"); Set<UserId> userIds = new HashSet<UserId>(); userIds.add(userId); Criteria criteria; criteria = createCriteriaFor(userIds, groupId, appId, options, token); List<Long> activityIdAsLongs = new ArrayList<Long>(activityIds.size()); for (String id : activityIds) { activityIdAsLongs.add(Long.parseLong(id)); } criteria.add(Restrictions.in("id", activityIdAsLongs)); // Obtains the count of activities that matches the search. long totalResults = (Long) criteria.setProjection( Projections.rowCount()).uniqueResult(); // Restore the original projection, removing the count. criteria.setProjection(null); criteria.setResultTransformer(Criteria.ROOT_ENTITY); List<Activity> activities; if (totalResults > 0) { // No need to execute the query if count(*) said 0. addOptionsToCriteria(criteria, options); activities = criteria.list(); } else { activities = new ArrayList<Activity>(); } log.trace("Leaving getActivities"); // Possible loss of precision, but we will hopefully never have more than // 2^31 activities. return ImmediateFuture.newInstance(new RestfulCollection<Activity>( activities, options.getFirst(), (int) totalResults, options.getMax())); } /** {@inheritDoc} * * This implementation ignores the groupId and the fields parameter. */ public Future<Activity> getActivity(final UserId userId, final GroupId groupId, final String appId, final Set<String> fields, final String activityId, final SecurityToken token) { log.trace("Entering getActivity"); Criteria criteria = getSession().createCriteria(Activity.class); criteria.add(Restrictions.eq("id", Long.parseLong(activityId))); Application app; app = applicationRepository.findApplicationByUrl(appId); criteria.add(Restrictions.eq("application", app)); Set<UserId> userIds = new HashSet<UserId>(); userIds.add(userId); addGroupFilterToCriteria(criteria, userIds, groupId, token, appId); Activity activity = (Activity) criteria.uniqueResult(); if (activity == null) { throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Activity not found"); } log.trace("Leaving getActivity"); return ImmediateFuture.newInstance(activity); } /** {@inheritDoc} * * This operation is not implemented, throws UnsupportedOperationException. */ public Future<Void> deleteActivities(final UserId userId, final GroupId groupId, final String appId, final Set<String> activityIds, final SecurityToken token) { log.trace("Entering deleteActivity"); throw new UnsupportedOperationException(); /* log.trace("Leaving deleteActivity"); return null; */ } /** {@inheritDoc} * * Creates (and persists) a new activity, obtaining the fields from the * provided activity. * * @param fields this parameter is ignored in this implementation. * * @param token an opaque token that represents the logged in user. It cannot * be null. * * TODO: decide what to do with the groupId parameter, it is not used in this * implementation, nor in the sample ActivityServiceDb implementation. */ public Future<Void> createActivity(final UserId userId, final GroupId groupId, final String appId, final Set<String> fields, final Activity activity, final SecurityToken token) { log.trace("Entering createActivity"); Application application; application = applicationRepository.findApplicationByUrl(appId); CoreUser user = (CoreUser) applicationRepository.getHibernateTemplate().get( CoreUser.class, Long.parseLong(userId.getUserId(token))); KatariActivity newActivity = new KatariActivity(new Date().getTime(), application, user, activity); getHibernateTemplate().saveOrUpdate(newActivity); log.trace("Leaving createActivity"); return null; } /** Creates a hibernate criteria for the provided conditions. * * This operation only considers the conditions that affect the number of * returned activities. The criteria is intended to be used to count the * number of matching activities and then add the sort conditions. * * @param userIds The user ids of the activities to search. It cannot be null. * * @param groupId only supports @self, that returns all the activities from * all the provided users. It cannot be null. * * @param appId The application id of activities. It cannot be null. * * @param options Options related to the resulting activities, like * filtering, etc. The returned criteria only contains the options that affect * the number or returned activities.. It cannot be null. * * @param token an opaque token that represents the logged in user. It cannot * be null. * * @return A criteria that matches the conditions inferred from the * parameters. */ private Criteria createCriteriaFor(final Set<UserId> userIds, final GroupId groupId, final String appId, final CollectionOptions options, final SecurityToken token) { log.trace("Entering createCriteriaFor"); Validate.notNull(userIds, "The list of user ids cannot be null."); Validate.notNull(groupId, "The group id cannot be null."); Validate.notNull(options, "The options cannot be null."); Validate.notNull(token, "The security token cannot be null."); Validate.isTrue(options.getMax() > 0, "You should ask for at least one row."); Criteria criteria = getSession().createCriteria(Activity.class); addGroupFilterToCriteria(criteria, userIds, groupId, token, appId); if (!appId.equals(newsFeedApplicationId)) { Application app; app = applicationRepository.findApplicationByUrl(appId); criteria.add(Restrictions.eq("application", app)); } if (options.getFilter() != null) { if (options.getFilterOperation() == null) { throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "If you request a filter, you must specify the filter operation."); } else if (options.getFilterOperation().equals(FilterOperation.present)) { criteria.add(Restrictions.isNotNull(options.getFilter())); } else { if (options.getFilterValue() == null) { throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "If you request a filter, you must specify the value."); } switch (options.getFilterOperation()) { case equals: criteria.add(Restrictions.eq(options.getFilter(), options.getFilterValue())); break; case startsWith: criteria.add(Restrictions.like(options.getFilter(), options.getFilterValue(), MatchMode.START)); break; case present: criteria.add(Restrictions.like(options.getFilter(), options.getFilterValue(), MatchMode.ANYWHERE)); break; default: throw new RuntimeException("Unsupported filter operation " + options.getFilterOperation()); } } } log.trace("Leaving createCriteriaFor"); return criteria; } /** Adds the group related query conditions to the criteria. * * @param criteria The criteria to modify. It cannot be null. * * @param userIds The user ids of the activities to search. It cannot be null. * * @param groupId only supports @self, that returns all the activities from * all the provided users. It cannot be null. * * @param token an opaque token that represents the logged in user. It cannot * be null. * * TODO: implement the other group types besides @self. */ private void addGroupFilterToCriteria(final Criteria criteria, final Set<UserId> userIds, final GroupId groupId, final SecurityToken token, final String appId) { log.trace("Entering addGroupFilterToCriteria"); Validate.notNull(criteria, "The criteria cannot be null."); Validate.notNull(userIds, "The userIds cannot be null."); Validate.notNull(groupId, "The group id cannot be null."); Validate.notNull(token, "The token cannot be null."); List<Long> userIdList = getUserIdList(userIds, token); if (katariActivityFilter != null) { katariActivityFilter.resolveSocialGraph(criteria, userIdList, groupId); } else { switch (groupId.getType()) { case self: if (userIdList.size() == 1) { criteria.createCriteria("user") .add(Restrictions.eq("id", userIdList.get(0))); } else { criteria.createCriteria("user").add(Restrictions.in("id", userIdList)); } break; default: throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Group parameter not supported"); } } log.trace("Leaving addGroupFilterToCriteria"); } /** Adds the option to the criteria that are not related to the number of * elements that match the search, like sort order and min/max results. * * @param criteria The criteria to modify. It cannot be null. * * @param options The options to add to the criteria. The options that are * considered here do not change the number activities that match the search * conditions. It cannot be null. */ private void addOptionsToCriteria(final Criteria criteria, final CollectionOptions options) { log.trace("Entering addOptionsToCriteria"); Validate.notNull(criteria, "The criteria cannot be null."); Validate.notNull(options, "The options cannot be null."); if (options.getSortBy() != null) { if (options.getSortOrder() == null) { criteria.addOrder(Order.asc(options.getSortBy())); } else { switch (options.getSortOrder()) { case ascending: criteria.addOrder(Order.asc(options.getSortBy())); break; case descending: criteria.addOrder(Order.desc(options.getSortBy())); break; default: throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Unrecognized sort order"); } } } criteria.setFirstResult(options.getFirst()); criteria.setMaxResults(options.getMax()); log.trace("Leaving addOptionsToCriteria"); } /** Returns a list with the id (as long) of each user id. * * @param userIds the set of user ids. It cannot be null. * * @param token the security token of the currently logged on user or * application. It cannot be null. * * @return the list of user id, as a list of long. Never returns null. */ private List<Long> getUserIdList(final Set<UserId> userIds, final SecurityToken token) { Validate.notNull(userIds, "The user ids cannot be null."); Validate.notNull(token, "The token cannot be null."); List<Long> result = new ArrayList<Long>(); for (UserId userId : userIds) { String uid = userId.getUserId(token); if (uid != null) { result.add(Long.parseLong(uid)); } } return result; } /** Sets the news feed application id. * @param theNewsFeedApplicationId the application id. */ public void setNewsFeedApplicationId(final String theNewsFeedApplicationId) { newsFeedApplicationId = theNewsFeedApplicationId; } /** Sets the Katari's activity filter. * @param filter the activity filter. */ public void setKatariActivityFilter(final KatariActivityFilter filter) { katariActivityFilter = filter; } }