/*
* Copyright 2010 Vodafone Group Services Ltd.
*
* Licensed 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.onesocialweb.openfire.manager;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.activity.InvalidActivityException;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.dom4j.dom.DOMDocument;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.onesocialweb.model.acl.AclAction;
import org.onesocialweb.model.acl.AclFactory;
import org.onesocialweb.model.acl.AclRule;
import org.onesocialweb.model.acl.AclSubject;
import org.onesocialweb.model.activity.ActivityActor;
import org.onesocialweb.model.activity.ActivityEntry;
import org.onesocialweb.model.activity.ActivityFactory;
import org.onesocialweb.model.activity.ActivityObject;
import org.onesocialweb.model.atom.AtomReplyTo;
import org.onesocialweb.model.atom.DefaultAtomHelper;
import org.onesocialweb.openfire.OswPlugin;
import org.onesocialweb.openfire.exception.AccessDeniedException;
import org.onesocialweb.openfire.handler.activity.PEPActivityHandler;
import org.onesocialweb.openfire.model.ActivityMessage;
import org.onesocialweb.openfire.model.PersistentActivityMessage;
import org.onesocialweb.openfire.model.PersistentSubscription;
import org.onesocialweb.openfire.model.Subscription;
import org.onesocialweb.openfire.model.acl.PersistentAclFactory;
import org.onesocialweb.openfire.model.activity.PersistentActivityEntry;
import org.onesocialweb.openfire.model.activity.PersistentActivityFactory;
import org.onesocialweb.xml.dom.ActivityDomWriter;
import org.onesocialweb.xml.dom.imp.DefaultActivityDomWriter;
import org.onesocialweb.xml.namespace.Atom;
import org.w3c.dom.Element;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
/**
* The activity manager is a singleton class taking care of all the business
* logic related to querying, creating, updating and deleting activities.
*
* @author eschenal
*
*/
public class ActivityManager {
/**
* Singleton: keep a static reference to teh only instance
*/
private static ActivityManager instance;
public static ActivityManager getInstance() {
if (instance == null) {
// Carefull, we are in a threaded environment !
synchronized (ActivityManager.class) {
instance = new ActivityManager();
}
}
return instance;
}
/**
* Class dependencies
* TODO Make this a true dependency injection
*/
private final ActivityFactory activityFactory;
private final AclFactory aclFactory;
/**
* Publish a new activity to the activity stream of the given user.
* activity-actor element is overwrittern using the user profile data to
* avoid spoofing. Notifications messages are sent to the users subscribed
* to this user activities.
*
* @param user
* The user who the activity belongs to
* @param entry
* The activity entry to publish
* @throws UserNotFoundException
*/
public void publishActivity(String userJID, ActivityEntry entry) throws UserNotFoundException {
// Overide the actor to avoid spoofing
User user = UserManager.getInstance().getUser(new JID(userJID).getNode());
ActivityActor actor = activityFactory.actor();
actor.setUri(userJID);
actor.setName(user.getName());
actor.setEmail(user.getEmail());
// Persist the activities
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
em.getTransaction().begin();
entry.setId(DefaultAtomHelper.generateId());
for (ActivityObject object : entry.getObjects()) {
object.setId(DefaultAtomHelper.generateId());
}
entry.setActor(actor);
entry.setPublished(Calendar.getInstance().getTime());
em.persist(entry);
em.getTransaction().commit();
em.close();
// Broadcast the notifications
notify(userJID, entry);
}
/**
* Updates an activity in the activity stream of the given user.
* activity-actor element is overwrittern using the user profile data to
* avoid spoofing. Notifications messages are sent to the users subscribed
* to this user activities.
*
* @param user
* The user who the activity belongs to
* @param entry
* The activity entry to update
* @throws UserNotFoundException
*/
public void updateActivity(String userJID, ActivityEntry entry) throws UserNotFoundException, UnauthorizedException {
// Overide the actor to avoid spoofing
User user = UserManager.getInstance().getUser(new JID(userJID).getNode());
ActivityActor actor = activityFactory.actor();
actor.setUri(userJID);
actor.setName(user.getName());
actor.setEmail(user.getEmail());
// Persist the activities
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
em.getTransaction().begin();
PersistentActivityEntry oldEntry=em.find(PersistentActivityEntry.class, entry.getId());
if ((oldEntry==null) || (!oldEntry.getActor().getUri().equalsIgnoreCase(userJID)))
throw new UnauthorizedException();
Date published=oldEntry.getPublished();
entry.setPublished(published);
entry.setUpdated(Calendar.getInstance().getTime());
em.remove(oldEntry);
entry.setActor(actor);
em.persist(entry);
em.getTransaction().commit();
em.close();
// Broadcast the notifications
notify(userJID, entry);
}
public void deleteActivity(String fromJID, String activityId) throws UnauthorizedException {
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
em.getTransaction().begin();
PersistentActivityEntry activity= em.find(PersistentActivityEntry.class, activityId);
if ((activity==null) || (!activity.getActor().getUri().equalsIgnoreCase(fromJID)))
throw new UnauthorizedException();
em.remove(activity);
em.getTransaction().commit();
em.close();
notifyDelete(fromJID, activityId);
}
public void deleteMessage(String activityId) {
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
em.getTransaction().begin();
Query query = em.createQuery("SELECT x FROM Messages x WHERE x.activity.id = ?1");
query.setParameter(1, activityId);
List<ActivityMessage> messages = query.getResultList();
for (ActivityMessage message:messages){
em.remove(message);
}
em.getTransaction().commit();
em.close();
}
/**
* Retrieve the last activities of the target user, taking into account the
* access rights of the requesting user.
*
* @param requestorJID
* the user requesting the activities
* @param targetJID
* the user whose activities are requested
* @return an immutable list of the last activities of the target entity that can be seen by the
* requesting entity
* @throws UserNotFoundException
*/
@SuppressWarnings("unchecked")
public List<ActivityEntry> getActivities(String requestorJID, String targetJID) throws UserNotFoundException {
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
Query query = em.createQuery("SELECT DISTINCT entry FROM ActivityEntry entry" + " JOIN entry.rules rule "
+ " JOIN rule.actions action " + " JOIN rule.subjects subject "
+ " WHERE entry.actor.uri = :target " + " AND action.name = :view "
+ " AND action.permission = :grant " + " AND (subject.type = :everyone "
+ " OR (subject.type = :group_type " + " AND subject.name IN (:groups)) "
+ " OR (subject.type = :person " + " AND subject.name = :jid)) ORDER BY entry.published DESC");
// Parametrize the query
query.setParameter("target", targetJID);
query.setParameter("view", AclAction.ACTION_VIEW);
query.setParameter("grant", AclAction.PERMISSION_GRANT);
query.setParameter("everyone", AclSubject.EVERYONE);
query.setParameter("group_type", AclSubject.GROUP);
query.setParameter("groups", getGroups(targetJID, requestorJID));
query.setParameter("person", AclSubject.PERSON);
query.setParameter("jid", requestorJID);
query.setMaxResults(20);
List<ActivityEntry> result = query.getResultList();
em.close();
return Collections.unmodifiableList(result);
}
/**
* Handle an activity pubsub event. Such a message is usually
* received by a user in these conditions: - the local user has subscribed
* to the remote user activities - the local user is "mentionned" in this
* activity - this activity relates to another activity of the local user
*
* @param remoteJID
* the entity sending the message
* @param localJID
* the entity having received the message
* @param activity
* the activity contained in the message
* @throws InvalidActivityException
* @throws AccessDeniedException
*/
public synchronized void handleMessage(String remoteJID, String localJID, ActivityEntry activity) throws InvalidActivityException, AccessDeniedException {
// Validate the activity
if (activity == null || !activity.hasId()) {
throw new InvalidActivityException();
}
// Create a message for the recipient
ActivityMessage message = new PersistentActivityMessage();
message.setSender(remoteJID);
message.setRecipient(localJID);
//in case of an activity update we keep the received date as the date of
//original publish, otherwise the updated posts will start showing on top of
// the inbox..which we agreed we don't want...
message.setReceived(activity.getPublished());
// Search if the activity exists in the database
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
PersistentActivityEntry previousActivity = em.find(PersistentActivityEntry.class, activity.getId());
// Assign the activity to the existing one if it exists
if (previousActivity != null) {
message.setActivity(previousActivity);
} else {
message.setActivity(activity);
}
//in case of an update the message will already exist in the DB
Query query = em.createQuery("SELECT x FROM Messages x WHERE x.activity.id = ?1");
query.setParameter(1, activity.getId());
List<ActivityMessage> messages = query.getResultList();
em.getTransaction().begin();
for (ActivityMessage oldMessage:messages){
if (oldMessage.getRecipient().equalsIgnoreCase(localJID))
em.remove(oldMessage);
}
em.getTransaction().commit();
// We go ahead and post the message to the recipient mailbox
em.getTransaction().begin();
em.persist(message);
em.getTransaction().commit();
em.close();
}
/**
* Subscribe an entity to another entity activities.
*
* @param from the subscriber
* @param to entity being subscribed to
* @throws AlreadySubscribed
*/
@SuppressWarnings("unchecked")
public synchronized void subscribe(String from, String to) {
// Check if it already exist
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
Query query = em.createQuery("SELECT x FROM Subscriptions x WHERE x.subscriber = ?1 AND x.target = ?2");
query.setParameter(1, from);
query.setParameter(2, to);
List<Subscription> subscriptions = query.getResultList();
// If already exist, we don't have anything left to do
if (subscriptions != null && subscriptions.size() > 0) {
em.close();
return;
}
// Add the subscription
Subscription subscription = new PersistentSubscription();
subscription.setSubscriber(from);
subscription.setTarget(to);
subscription.setCreated(Calendar.getInstance().getTime());
// Store
em.getTransaction().begin();
em.persist(subscription);
em.getTransaction().commit();
em.close();
}
/**
* Delete a subscription.
*
* @param from the entity requesting to unsubscribe
* @param to the subscription target
* @throws SubscriptionNotFound
*/
@SuppressWarnings("unchecked")
public synchronized void unsubscribe(String from, String to) {
EntityManager em = OswPlugin.getEmFactory().createEntityManager();
// Check if it already exist
Query query = em.createQuery("SELECT x FROM Subscriptions x WHERE x.subscriber = ?1 AND x.target = ?2");
query.setParameter(1, from);
query.setParameter(2, to);
List<Subscription> subscriptions = query.getResultList();
// If it does not exist, we don't have anything left to do
if (subscriptions == null || subscriptions.size()== 0) {
em.close();
return;
}
// Remove the subscriptions
// There should never be more than one.. but better safe than sorry
em.getTransaction().begin();
for (Subscription activitySubscription : subscriptions) {
em.remove(activitySubscription);
}
em.getTransaction().commit();
em.close();
}
@SuppressWarnings("unchecked")
public List<Subscription> getSubscribers(String targetJID) {
// Get a list of people who are interested by this stuff
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
Query query = em.createQuery("SELECT x FROM Subscriptions x WHERE x.target = ?1");
query.setParameter(1, targetJID);
List<Subscription> subscriptions = query.getResultList();
em.close();
return subscriptions;
}
@SuppressWarnings("unchecked")
public List<Subscription> getSubscriptions(String subscriberJID) {
// Get a list of people who are interested by this stuff
final EntityManager em = OswPlugin.getEmFactory().createEntityManager();
Query query = em.createQuery("SELECT x FROM Subscriptions x WHERE x.subscriber = ?1");
query.setParameter(1, subscriberJID);
List<Subscription> subscriptions = query.getResultList();
em.close();
return subscriptions;
}
private void notify(String fromJID, ActivityEntry entry) throws UserNotFoundException {
// TODO We may want to do some cleaning of activities before
// forwarding them (e.g. remove the acl, it is no one business)
final ActivityDomWriter writer = new DefaultActivityDomWriter();
final XMPPServer server = XMPPServer.getInstance();
final List<Subscription> subscriptions = getSubscribers(fromJID);
// final Roster roster = XMPPServer.getInstance().getRosterManager().getRoster(new JID(fromJID).getNode());
final DOMDocument domDocument = new DOMDocument();
// Prepare the message
final Element entryElement = (Element) domDocument.appendChild(domDocument.createElementNS(Atom.NAMESPACE, Atom.ENTRY_ELEMENT));
writer.write(entry, entryElement);
domDocument.removeChild(entryElement);
final Message message = new Message();
message.setFrom(fromJID);
message.setBody("New activity: " + entry.getTitle());
message.setType(Message.Type.headline);
org.dom4j.Element eventElement = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
org.dom4j.Element itemsElement = eventElement.addElement("items");
itemsElement.addAttribute("node", PEPActivityHandler.NODE);
org.dom4j.Element itemElement = itemsElement.addElement("item");
itemElement.addAttribute("id", entry.getId());
itemElement.add((org.dom4j.Element) entryElement);
// Keep a list of people we sent it to avoid duplicates
List<String> alreadySent = new ArrayList<String>();
// Send to this user
alreadySent.add(fromJID);
message.setTo(fromJID);
server.getMessageRouter().route(message);
// Send to all subscribers
for (Subscription activitySubscription : subscriptions) {
String recipientJID = activitySubscription.getSubscriber();
if (!canSee(fromJID, entry, recipientJID)) {
continue;
}
alreadySent.add(recipientJID);
message.setTo(recipientJID);
server.getMessageRouter().route(message);
}
// Send to recipients, if they can see it and have not already received it
if (entry.hasRecipients()) {
for (AtomReplyTo recipient : entry.getRecipients()) {
//TODO This is dirty, the recipient should be an IRI etc...
String recipientJID = recipient.getHref();
if (!alreadySent.contains(recipientJID) && canSee(fromJID, entry, recipientJID)) {
alreadySent.add(fromJID);
message.setTo(recipientJID);
server.getMessageRouter().route(message);
}
}
}
}
private void notifyDelete(String fromJID, String activityId) {
final XMPPServer server = XMPPServer.getInstance();
final List<Subscription> subscriptions = getSubscribers(fromJID);
// Prepare the message
final Message message = new Message();
message.setFrom(fromJID);
message.setBody("Delete activity: " + activityId);
message.setType(Message.Type.headline);
org.dom4j.Element eventElement = message.addChildElement("event", "http://jabber.org/protocol/pubsub#event");
org.dom4j.Element itemsElement = eventElement.addElement("items");
itemsElement.addAttribute("node", PEPActivityHandler.NODE);
org.dom4j.Element retractElement = itemsElement.addElement("retract");
retractElement.addAttribute("id", activityId);
// Keep a list of people we sent it to avoid duplicates
List<String> alreadySent = new ArrayList<String>();
// Send to this user
alreadySent.add(fromJID);
message.setTo(fromJID);
server.getMessageRouter().route(message);
// Send to all subscribers
for (Subscription activitySubscription : subscriptions) {
String recipientJID = activitySubscription.getSubscriber();
alreadySent.add(recipientJID);
message.setTo(recipientJID);
server.getMessageRouter().route(message);
}
}
private List<String> getGroups(String ownerJID, String userJID) {
RosterManager rosterManager = XMPPServer.getInstance().getRosterManager();
Roster roster;
try {
roster = rosterManager.getRoster(new JID(ownerJID).getNode());
RosterItem rosterItem = roster.getRosterItem(new JID(userJID));
if (rosterItem != null) {
return rosterItem.getGroups();
}
} catch (UserNotFoundException e) {
}
return new ArrayList<String>();
}
private boolean canSee(String fromJID, ActivityEntry entry, String viewer) throws UserNotFoundException {
// Get a view action
final AclAction viewAction = aclFactory.aclAction(AclAction.ACTION_VIEW, AclAction.PERMISSION_GRANT);
AclRule rule = null;
for (AclRule aclRule : entry.getAclRules()) {
if (aclRule.hasAction(viewAction)) {
rule = aclRule;
break;
}
}
// If no view action was found, we consider it is denied
if (rule == null)
return false;
return AclManager.canSee(fromJID, rule, viewer);
}
/**
* Private constructor to enforce the singleton
*/
private ActivityManager() {
activityFactory = new PersistentActivityFactory();
aclFactory = new PersistentAclFactory();
}
}