/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* 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.jivesoftware.openfire.pep;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.commands.AdHocCommandManager;
import org.jivesoftware.openfire.entitycaps.EntityCapabilities;
import org.jivesoftware.openfire.entitycaps.EntityCapabilitiesManager;
import org.jivesoftware.openfire.pubsub.CollectionNode;
import org.jivesoftware.openfire.pubsub.DefaultNodeConfiguration;
import org.jivesoftware.openfire.pubsub.LeafNode;
import org.jivesoftware.openfire.pubsub.Node;
import org.jivesoftware.openfire.pubsub.NodeSubscription;
import org.jivesoftware.openfire.pubsub.PendingSubscriptionsCommand;
import org.jivesoftware.openfire.pubsub.PubSubEngine;
import org.jivesoftware.openfire.pubsub.PubSubPersistenceManager;
import org.jivesoftware.openfire.pubsub.PubSubService;
import org.jivesoftware.openfire.pubsub.PublishedItem;
import org.jivesoftware.openfire.pubsub.PublishedItemTask;
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.pubsub.models.PublisherModel;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.FastDateFormat;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.cache.Cacheable;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketExtension;
/**
* A PEPService is a {@link PubSubService} for use with XEP-0163: "Personal Eventing via
* Pubsub" Version 1.0
*
* Note: Although this class implements {@link Cacheable}, instances should only be
* cached in caches that have time-based (as opposed to size-based) eviction policies.
*
* @author Armando Jagucki
*/
public class PEPService implements PubSubService, Cacheable {
/**
* Timer to save published items to the database or remove deleted or old
* items.
*/
private static final Timer timer = new Timer("PEP service maintenance");
/**
* Date format to use for time stamps in delayed event notifications.
*/
private static final FastDateFormat fastDateFormat;
/**
* The bare JID that this service is identified by.
*/
private String serviceOwnerJID;
/**
* Collection node that acts as the root node of the entire node hierarchy.
*/
private CollectionNode rootCollectionNode = null;
/**
* Nodes managed by this service, table: key nodeID (String); value Node
*/
private Map<String, Node> nodes = new ConcurrentHashMap<String, Node>();
/**
* The packet router for the server.
*/
private PacketRouter router = null;
/**
* Default configuration to use for newly created leaf nodes.
*/
private DefaultNodeConfiguration leafDefaultConfiguration;
/**
* Default configuration to use for newly created collection nodes.
*/
private DefaultNodeConfiguration collectionDefaultConfiguration;
/**
* Returns the permission policy for creating nodes. A true value means that
* not anyone can create a node, only the service admin.
*/
private boolean nodeCreationRestricted = true;
/**
* Keep a registry of the presence's show value of users that subscribed to
* a node of the pep service and for which the node only delivers
* notifications for online users or node subscriptions deliver events based
* on the user presence show value. Offline users will not have an entry in
* the map. Note: Key-> bare JID and Value-> Map whose key is full JID of
* connected resource and value is show value of the last received presence.
*/
private Map<String, Map<String, String>> barePresences = new ConcurrentHashMap<String, Map<String, String>>();
/**
* Queue that holds the items that need to be added to the database.
*/
private Queue<PublishedItem> itemsToAdd = new LinkedBlockingQueue<PublishedItem>(10000);
/**
* Queue that holds the items that need to be deleted from the database.
*/
private Queue<PublishedItem> itemsToDelete = new LinkedBlockingQueue<PublishedItem>(10000);
/**
* Manager that keeps the list of ad-hoc commands and processing command
* requests.
*/
private AdHocCommandManager adHocCommandManager;
/**
* Used to handle filtered-notifications.
*/
private EntityCapabilitiesManager entityCapsManager = EntityCapabilitiesManager.getInstance();
/**
* The time to elapse between each execution of the maintenance process.
* Default is 2 minutes.
*/
private int items_task_timeout = 2 * 60 * 1000;
/**
* Task that saves or deletes published items from the database.
*/
private PublishedItemTask publishedItemTask;
static {
fastDateFormat = FastDateFormat.getInstance(JiveConstants.XMPP_DATETIME_FORMAT, TimeZone.getTimeZone("UTC"));
}
/**
* Constructs a PEPService.
*
* @param server the XMPP server.
* @param bareJID the bare JID (service ID) of the user owning the service.
*/
public PEPService(XMPPServer server, String bareJID) {
this.serviceOwnerJID = bareJID;
router = server.getPacketRouter();
// Initialize the ad-hoc commands manager to use for this pep service
adHocCommandManager = new AdHocCommandManager();
adHocCommandManager.addCommand(new PendingSubscriptionsCommand(this));
// Save or delete published items from the database every 2 minutes
// starting in 2 minutes (default values)
publishedItemTask = new PublishedItemTask(this) {
};
timer.schedule(publishedItemTask, items_task_timeout, items_task_timeout);
// Load default configuration for leaf nodes
leafDefaultConfiguration = PubSubPersistenceManager.loadDefaultConfiguration(this, true);
if (leafDefaultConfiguration == null) {
// Create and save default configuration for leaf nodes;
leafDefaultConfiguration = new DefaultNodeConfiguration(true);
leafDefaultConfiguration.setAccessModel(AccessModel.presence);
leafDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
leafDefaultConfiguration.setDeliverPayloads(true);
leafDefaultConfiguration.setLanguage("English");
leafDefaultConfiguration.setMaxPayloadSize(5120);
leafDefaultConfiguration.setNotifyConfigChanges(true);
leafDefaultConfiguration.setNotifyDelete(true);
leafDefaultConfiguration.setNotifyRetract(true);
leafDefaultConfiguration.setPersistPublishedItems(false);
leafDefaultConfiguration.setMaxPublishedItems(-1);
leafDefaultConfiguration.setPresenceBasedDelivery(false);
leafDefaultConfiguration.setSendItemSubscribe(true);
leafDefaultConfiguration.setSubscriptionEnabled(true);
leafDefaultConfiguration.setReplyPolicy(null);
PubSubPersistenceManager.createDefaultConfiguration(this, leafDefaultConfiguration);
}
// Load default configuration for collection nodes
collectionDefaultConfiguration = PubSubPersistenceManager.loadDefaultConfiguration(this, false);
if (collectionDefaultConfiguration == null) {
// Create and save default configuration for collection nodes;
collectionDefaultConfiguration = new DefaultNodeConfiguration(false);
collectionDefaultConfiguration.setAccessModel(AccessModel.presence);
collectionDefaultConfiguration.setPublisherModel(PublisherModel.publishers);
collectionDefaultConfiguration.setDeliverPayloads(false);
collectionDefaultConfiguration.setLanguage("English");
collectionDefaultConfiguration.setNotifyConfigChanges(true);
collectionDefaultConfiguration.setNotifyDelete(true);
collectionDefaultConfiguration.setNotifyRetract(true);
collectionDefaultConfiguration.setPresenceBasedDelivery(false);
collectionDefaultConfiguration.setSubscriptionEnabled(true);
collectionDefaultConfiguration.setReplyPolicy(null);
collectionDefaultConfiguration.setAssociationPolicy(CollectionNode.LeafNodeAssociationPolicy.all);
collectionDefaultConfiguration.setMaxLeafNodes(-1);
PubSubPersistenceManager.createDefaultConfiguration(this, collectionDefaultConfiguration);
}
// Load nodes to memory
PubSubPersistenceManager.loadNodes(this);
// Ensure that we have a root collection node
if (nodes.isEmpty()) {
// Create root collection node
JID creatorJID = new JID(bareJID);
rootCollectionNode = new CollectionNode(this, null, bareJID, creatorJID);
// Add the creator as the node owner
rootCollectionNode.addOwner(creatorJID);
// Save new root node
rootCollectionNode.saveToDB();
}
else {
rootCollectionNode = (CollectionNode) getNode(bareJID);
}
}
public void addNode(Node node) {
nodes.put(node.getNodeID(), node);
}
public void removeNode(String nodeID) {
nodes.remove(nodeID);
}
public Node getNode(String nodeID) {
return nodes.get(nodeID);
}
public Collection<Node> getNodes() {
return nodes.values();
}
public CollectionNode getRootCollectionNode() {
return rootCollectionNode;
}
public JID getAddress() {
return new JID(serviceOwnerJID);
}
public String getServiceID() {
// The bare JID of the user is the service ID for PEP
return serviceOwnerJID;
}
public DefaultNodeConfiguration getDefaultNodeConfiguration(boolean leafType) {
if (leafType) {
return leafDefaultConfiguration;
}
return collectionDefaultConfiguration;
}
public Collection<String> getShowPresences(JID subscriber) {
return PubSubEngine.getShowPresences(this, subscriber);
}
public boolean canCreateNode(JID creator) {
// Node creation is always allowed for sysadmin
if (isNodeCreationRestricted() && !isServiceAdmin(creator)) {
// The user is not allowed to create nodes
return false;
}
return true;
}
/**
* Returns true if the the prober is allowed to see the presence of the probee.
*
* @param prober the user that is trying to probe the presence of another user.
* @param probee the username of the uset that is being probed.
* @return true if the the prober is allowed to see the presence of the probee.
* @throws UserNotFoundException If the probee does not exist in the local server or the prober
* is not present in the roster of the probee.
*/
private boolean canProbePresence(JID prober, JID probee) throws UserNotFoundException {
Roster roster;
roster = XMPPServer.getInstance().getRosterManager().getRoster(prober.getNode());
RosterItem item = roster.getRosterItem(probee);
if (item.getSubStatus() == RosterItem.SUB_BOTH || item.getSubStatus() == RosterItem.SUB_FROM) {
return true;
}
return false;
}
public boolean isCollectionNodesSupported() {
return true;
}
public boolean isInstantNodeSupported() {
return true;
}
public boolean isMultipleSubscriptionsEnabled() {
return false;
}
public boolean isServiceAdmin(JID user) {
// Here we consider a 'service admin' to be the user that this PEPService
// is associated with.
if (serviceOwnerJID.equals(user.toBareJID())) {
return true;
}
else {
return false;
}
}
public boolean isNodeCreationRestricted() {
return nodeCreationRestricted;
}
public void presenceSubscriptionNotRequired(Node node, JID user) {
PubSubEngine.presenceSubscriptionNotRequired(this, node, user);
}
public void presenceSubscriptionRequired(Node node, JID user) {
PubSubEngine.presenceSubscriptionRequired(this, node, user);
}
public void send(Packet packet) {
router.route(packet);
}
public void broadcast(Node node, Message message, Collection<JID> jids) {
message.setFrom(getAddress());
for (JID jid : jids) {
message.setTo(jid);
message.setID(node.getNodeID() + "__" + jid.toBareJID() + "__" + StringUtils.randomString(5));
router.route(message);
}
}
public void sendNotification(Node node, Message message, JID recipientJID) {
message.setTo(recipientJID);
message.setFrom(getAddress());
message.setID(node.getNodeID() + "__" + recipientJID.toBareJID() + "__" + StringUtils.randomString(5));
// If the recipient subscribed with a bare JID and this PEPService can retrieve
// presence information for the recipient, collect all of their full JIDs and
// send the notification to each below.
Set<JID> recipientFullJIDs = new HashSet<JID>();
if (XMPPServer.getInstance().isLocal(recipientJID)) {
if (recipientJID.getResource() == null) {
for (ClientSession clientSession : SessionManager.getInstance().getSessions(recipientJID.getNode())) {
recipientFullJIDs.add(clientSession.getAddress());
}
}
}
else {
// Since recipientJID is not local, try to get presence info from cached known remote
// presences.
Map<String, Set<JID>> knownRemotePresences = XMPPServer.getInstance().getIQPEPHandler().getKnownRemotePresenes();
Set<JID> remotePresenceSet = knownRemotePresences.get(getAddress().toBareJID());
if (remotePresenceSet != null) {
for (JID remotePresence : remotePresenceSet) {
if (recipientJID.toBareJID().equals(remotePresence.toBareJID())) {
recipientFullJIDs.add(remotePresence);
}
}
}
}
if (recipientFullJIDs.isEmpty()) {
router.route(message);
return;
}
for (JID recipientFullJID : recipientFullJIDs) {
// Include an Extended Stanza Addressing "replyto" extension specifying the publishing
// resource. However, only include the extension if the receiver has a presence subscription
// to the service owner.
try {
JID publisher = null;
// Get the ID of the node that had an item published to or retracted from.
Element itemsElement = message.getElement().element("event").element("items");
String nodeID = itemsElement.attributeValue("node");
// Get the ID of the item that was published or retracted.
String itemID = null;
Element itemElement = itemsElement.element("item");
if (itemElement == null) {
Element retractElement = itemsElement.element("retract");
if (retractElement != null) {
itemID = retractElement.attributeValue("id");
}
}
else {
itemID = itemElement.attributeValue("id");
}
// Check if the recipientFullJID is interested in notifications for this node.
// If the recipient has not yet requested any notification filtering, continue and send
// the notification.
EntityCapabilities entityCaps = entityCapsManager.getEntityCapabilities(recipientFullJID);
if (entityCaps != null) {
if (!entityCaps.containsFeature(nodeID + "+notify")) {
return;
}
}
// Get the full JID of the item publisher from the node that was published to.
// This full JID will be used as the "replyto" address in the addressing extension.
if (node.isCollectionNode()) {
for (Node leafNode : node.getNodes()) {
if (leafNode.getNodeID().equals(nodeID)) {
publisher = leafNode.getPublishedItem(itemID).getPublisher();
// Ensure the recipientJID has access to receive notifications for items published to the leaf node.
AccessModel accessModel = leafNode.getAccessModel();
if (!accessModel.canAccessItems(leafNode, recipientFullJID, publisher)) {
return;
}
break;
}
}
}
else {
publisher = node.getPublishedItem(itemID).getPublisher();
}
// Ensure the recipient is subscribed to the service owner's (publisher's) presence.
if (canProbePresence(publisher, recipientFullJID)) {
Element addresses = DocumentHelper.createElement(QName.get("addresses", "http://jabber.org/protocol/address"));
Element address = addresses.addElement("address");
address.addAttribute("type", "replyto");
address.addAttribute("jid", publisher.toString());
Message extendedMessage = message.createCopy();
extendedMessage.addExtension(new PacketExtension(addresses));
extendedMessage.setTo(recipientFullJID);
router.route(extendedMessage);
}
}
catch (IndexOutOfBoundsException e) {
// Do not add addressing extension to message.
}
catch (UserNotFoundException e) {
// Do not add addressing extension to message.
router.route(message);
}
catch (NullPointerException e) {
try {
if (canProbePresence(getAddress(), recipientFullJID)) {
message.setTo(recipientFullJID);
}
}
catch (UserNotFoundException e1) {
// Do nothing
}
router.route(message);
}
}
}
/**
* Sends an event notification for the last published item of each leaf node under the
* root collection node to the recipient JID. If the recipient has no subscription to
* the root collection node, has not yet been authorized, or is pending to be
* configured -- then no notifications are going to be sent.<p>
*
* Depending on the subscription configuration the event notifications may or may not have
* a payload, may not be sent if a keyword (i.e. filter) was defined and it was not matched.
*
* @param recipientJID the recipient that is to receive the last published item notifications.
*/
public void sendLastPublishedItems(JID recipientJID) {
// Ensure the recipient has a subscription to this service's root collection node.
NodeSubscription subscription = rootCollectionNode.getSubscription(recipientJID);
if (subscription == null) {
subscription = rootCollectionNode.getSubscription(new JID(recipientJID.toBareJID()));
}
if (subscription == null) {
return;
}
// Send the last published item of each leaf node to the recipient.
for (Node leafNode : rootCollectionNode.getNodes()) {
// Retrieve last published item for the leaf node.
PublishedItem leafLastPublishedItem = null;
leafLastPublishedItem = leafNode.getLastPublishedItem();
if (leafLastPublishedItem == null) {
continue;
}
// Check if the published item can be sent to the subscriber
if (!subscription.canSendPublicationEvent(leafLastPublishedItem.getNode(), leafLastPublishedItem)) {
return;
}
// Send event notification to the subscriber
Message notification = new Message();
Element event = notification.getElement().addElement("event", "http://jabber.org/protocol/pubsub#event");
Element items = event.addElement("items");
items.addAttribute("node", leafLastPublishedItem.getNode().getNodeID());
Element item = items.addElement("item");
if (((LeafNode) leafLastPublishedItem.getNode()).isItemRequired()) {
item.addAttribute("id", leafLastPublishedItem.getID());
}
if (leafLastPublishedItem.getNode().isPayloadDelivered() && leafLastPublishedItem.getPayload() != null) {
item.add(leafLastPublishedItem.getPayload().createCopy());
}
// Add a message body (if required)
if (subscription.isIncludingBody()) {
notification.setBody(LocaleUtils.getLocalizedString("pubsub.notification.message.body"));
}
// Include date when published item was created
notification.getElement().addElement("delay", "urn:xmpp:delay").addAttribute("stamp", fastDateFormat.format(leafLastPublishedItem.getCreationDate()));
// Send the event notification to the subscriber
this.sendNotification(subscription.getNode(), notification, subscription.getJID());
}
}
public void queueItemToAdd(PublishedItem newItem) {
PubSubEngine.queueItemToAdd(this, newItem);
}
public void queueItemToRemove(PublishedItem removedItem) {
PubSubEngine.queueItemToRemove(this, removedItem);
}
public Map<String, Map<String, String>> getBarePresences() {
return barePresences;
}
public Queue<PublishedItem> getItemsToAdd() {
return itemsToAdd;
}
public Queue<PublishedItem> getItemsToDelete() {
return itemsToDelete;
}
public AdHocCommandManager getManager() {
return adHocCommandManager;
}
public PublishedItemTask getPublishedItemTask() {
return publishedItemTask;
}
public void setPublishedItemTask(PublishedItemTask task) {
publishedItemTask = task;
}
public Timer getTimer() {
return timer;
}
public int getItemsTaskTimeout() {
return items_task_timeout;
}
public void setItemsTaskTimeout(int timeout) {
items_task_timeout = timeout;
}
public int getCachedSize() {
// Rather arbitrary. Don't use this for size-based eviction policies!
return 600;
}
}