Commit f02f093f authored by Alex Wenckus's avatar Alex Wenckus Committed by alex

Initial check-in

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/branches@8733 b35dd754-fafc-0310-a699-88a17e54d16e
parent 70fade12
...@@ -112,21 +112,18 @@ public class AdHocCommandHandler extends IQHandler ...@@ -112,21 +112,18 @@ public class AdHocCommandHandler extends IQHandler
} }
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
List<Element> answer = new ArrayList<Element>(); List<DiscoItem> answer = new ArrayList<DiscoItem>();
if (!NAMESPACE.equals(node)) { if (!NAMESPACE.equals(node)) {
answer = Collections.emptyList(); answer = Collections.emptyList();
} }
else { else {
Element item;
for (AdHocCommand command : manager.getCommands()) { for (AdHocCommand command : manager.getCommands()) {
// Only include commands that the sender can invoke (i.e. has enough permissions) // Only include commands that the sender can invoke (i.e. has enough permissions)
if (command.hasPermission(senderJID)) { if (command.hasPermission(senderJID)) {
item = DocumentHelper.createElement("item"); final DiscoItem item = new DiscoItem(new JID(serverName),
item.addAttribute("jid", serverName); command.getLabel(), command.getCode(), null);
item.addAttribute("node", command.getCode());
item.addAttribute("name", command.getLabel());
answer.add(item); answer.add(item);
} }
} }
......
...@@ -445,8 +445,7 @@ public class InternalComponentManager extends BasicModule implements ComponentMa ...@@ -445,8 +445,7 @@ public class InternalComponentManager extends BasicModule implements ComponentMa
return; return;
} }
try { try {
XMPPServer.getInstance().getIQDiscoItemsHandler().addComponentItem(packet.getFrom() XMPPServer.getInstance().getIQDiscoItemsHandler().addComponentItem(packet.getFrom(),
.toBareJID(),
identity.attributeValue("name")); identity.attributeValue("name"));
if (component instanceof ComponentSession.ExternalComponent) { if (component instanceof ComponentSession.ExternalComponent) {
ComponentSession.ExternalComponent externalComponent = ComponentSession.ExternalComponent externalComponent =
......
...@@ -11,51 +11,169 @@ ...@@ -11,51 +11,169 @@
package org.jivesoftware.openfire.disco; package org.jivesoftware.openfire.disco;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.jivesoftware.util.rsm.Result;
import org.xmpp.packet.JID;
/** /**
* An item is associated with an XMPP Entity, usually thought of a children of the parent * <p>
* entity and normally are addressable as a JID.<p> * An item is associated with an XMPP Entity, usually thought of a children of
* <p/> * the parent entity and normally are addressable as a JID.
* An item associated with an entity may not be addressable as a JID. In order to handle * </p>
* such items, Service Discovery uses an optional 'node' attribute that supplements the * <p>
* 'jid' attribute. * An item associated with an entity may not be addressable as a JID. In order
* to handle such items, Service Discovery uses an optional 'node' attribute
* that supplements the 'jid' attribute.
* </p>
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public interface DiscoItem { public class DiscoItem implements Result {
private final JID jid;
private final String name;
private final String node;
private final String action;
/**
* <p>
* Creates a new DiscoItem instance.
* </p>
*
* @param jid
* specifies the Jabber ID of the item "owner" or location
* (required).
* @param name
* specifies a natural-language name for the item (can be null).
* @param node
* specifies the particular node associated with the JID of the
* item "owner" or location (can be null).
* @param action
* specifies the action to be taken for the item.
* @throws IllegalArgumentException
* If a required parameter was null, or if the supplied 'action'
* parameter has another value than 'null', "update" or
* "remove".
*/
public DiscoItem(JID jid, String name, String node, String action) {
if (jid == null) {
throw new IllegalArgumentException("Argument 'jid' cannot be null.");
}
if (action != null && !action.equals("update")
&& !action.equals("remove")) {
throw new IllegalArgumentException(
"Argument 'jid' cannot have any other value than null, \"update\" or \"remove\".");
}
this.jid = jid;
this.name = name;
this.node = node;
this.action = action;
}
/** /**
* <p>
* Returns the entity's ID. * Returns the entity's ID.
* </p>
* *
* @return the entity's ID. * @return the entity's ID.
*/ */
public abstract String getJID(); public JID getJID() {
return jid;
}
/** /**
* Returns the node attribute that supplements the 'jid' attribute. A node is merely * <p>
* something that is associated with a JID and for which the JID can provide information.<p> * Returns the node attribute that supplements the 'jid' attribute. A node
* <p/> * is merely something that is associated with a JID and for which the JID
* Node attributes SHOULD be used only when trying to provide or query information which * can provide information.
* is not directly addressable. * </p>
* <p>
* Node attributes SHOULD be used only when trying to provide or query
* information which is not directly addressable.
* </p>
* *
* @return the node attribute that supplements the 'jid' attribute * @return the node attribute that supplements the 'jid' attribute
*/ */
public abstract String getNode(); public String getNode() {
return node;
}
/** /**
* Returns the entity's name. The entity's name specifies in natural-language the name for the * <p>
* item. * Returns the entity's name. The entity's name specifies in
* natural-language the name for the item.
* </p>
* *
* @return the entity's name. * @return the entity's name.
*/ */
public abstract String getName(); public String getName() {
return name;
}
/** /**
* Returns the action (i.e. update or remove) that indicates what must be done with this item or * <p>
* null if none. An "update" action requests the server to create or update the item. Whilst a * Returns the action (i.e. update or remove) that indicates what must be
* "remove" action requests to remove the item. * done with this item or null if none. An "update" action requests the
* server to create or update the item. Whilst a "remove" action requests to
* remove the item.
* </p>
* *
* @return the action (i.e. update or remove) that indicates what must be done with this item or * @return the action (i.e. update or remove) that indicates what must be
* null if none. * done with this item or null if none.
*/ */
public abstract String getAction(); public String getAction() {
return action;
}
/**
* Returns a dom4j element that represents this DiscoItem object.
*
* @return element representing this object.
*/
public Element getElement() {
final Element element = DocumentHelper.createElement("item");
if (action != null) {
element.addAttribute("action", action);
}
if (jid != null) {
element.addAttribute("jid", jid.toString());
}
if (name != null) {
element.addAttribute("name", name);
}
if (node != null) {
element.addAttribute("node", node);
}
return element;
}
/*
* (non-Javadoc)
*
* @see org.jivesoftware.util.resultsetmanager.Result#getUID()
*/
public String getUID() {
final StringBuilder sb = new StringBuilder(jid.toString());
if (name != null) {
sb.append(name);
}
if (node != null) {
sb.append(node);
}
if (action != null) {
sb.append(action);
}
return sb.toString();
}
} }
...@@ -11,11 +11,10 @@ ...@@ -11,11 +11,10 @@
package org.jivesoftware.openfire.disco; package org.jivesoftware.openfire.disco;
import org.dom4j.Element;
import org.xmpp.packet.JID;
import java.util.Iterator; import java.util.Iterator;
import org.xmpp.packet.JID;
/** /**
* A DiscoItemsProvider is responsible for providing the items associated with a JID's name and * A DiscoItemsProvider is responsible for providing the items associated with a JID's name and
* node. For example, the room service could implement this interface in order to provide * node. For example, the room service could implement this interface in order to provide
...@@ -30,7 +29,7 @@ import java.util.Iterator; ...@@ -30,7 +29,7 @@ import java.util.Iterator;
public interface DiscoItemsProvider { public interface DiscoItemsProvider {
/** /**
* Returns an Iterator (of Element) with the target entity's items or null if none. Each Element * Returns an Iterator (of DiscoItem) with the target entity's items or null if none. Each DiscoItem
* must include a JID attribute and may include the name and node attributes of the entity. In * must include a JID attribute and may include the name and node attributes of the entity. In
* case that the sender of the disco request is not authorized to discover items an * case that the sender of the disco request is not authorized to discover items an
* UnauthorizedException will be thrown. * UnauthorizedException will be thrown.
...@@ -38,8 +37,8 @@ public interface DiscoItemsProvider { ...@@ -38,8 +37,8 @@ public interface DiscoItemsProvider {
* @param name the recipient JID's name. * @param name the recipient JID's name.
* @param node the requested disco node. * @param node the requested disco node.
* @param senderJID the XMPPAddress of user that sent the disco items request. * @param senderJID the XMPPAddress of user that sent the disco items request.
* @return an Iterator (of Element) with the target entity's items or null if none. * @return an Iterator (of DiscoItem) with the target entity's items or null if none.
*/ */
public abstract Iterator<Element> getItems(String name, String node, JID senderJID); public abstract Iterator<DiscoItem> getItems(String name, String node, JID senderJID);
} }
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
package org.jivesoftware.openfire.disco; package org.jivesoftware.openfire.disco;
import org.xmpp.packet.JID;
/** /**
* Represent a DiscoItem provided by the server. Therefore, the DiscoServerItems are responsible * Represent a DiscoItem provided by the server. Therefore, the DiscoServerItems are responsible
* for providing the DiscoInfoProvider and DiscoItemsProvider that will provide the information and * for providing the DiscoInfoProvider and DiscoItemsProvider that will provide the information and
...@@ -26,8 +28,29 @@ package org.jivesoftware.openfire.disco; ...@@ -26,8 +28,29 @@ package org.jivesoftware.openfire.disco;
* *
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public interface DiscoServerItem extends DiscoItem { public class DiscoServerItem extends DiscoItem {
private final DiscoInfoProvider infoProvider;
private final DiscoItemsProvider itemsProvider;
public DiscoServerItem(JID jid, String name, String node, String action,
DiscoInfoProvider infoProvider, DiscoItemsProvider itemsProvider)
{
super(jid, name, node, action);
if (infoProvider == null)
{
throw new IllegalArgumentException("Argument 'infoProvider' cannot be null.");
}
if (itemsProvider == null)
{
throw new IllegalArgumentException("Argument 'itemsProvider' cannot be null.");
}
this.infoProvider = infoProvider;
this.itemsProvider = itemsProvider;
}
/** /**
* Returns the DiscoInfoProvider responsible for providing the information related to this item. * Returns the DiscoInfoProvider responsible for providing the information related to this item.
* The DiscoInfoProvider will be automatically included in IQDiscoInfoHandler as the provider * The DiscoInfoProvider will be automatically included in IQDiscoInfoHandler as the provider
...@@ -35,7 +58,9 @@ public interface DiscoServerItem extends DiscoItem { ...@@ -35,7 +58,9 @@ public interface DiscoServerItem extends DiscoItem {
* *
* @return the DiscoInfoProvider responsible for providing the information related to this item. * @return the DiscoInfoProvider responsible for providing the information related to this item.
*/ */
public abstract DiscoInfoProvider getDiscoInfoProvider(); public DiscoInfoProvider getDiscoInfoProvider() {
return infoProvider;
}
/** /**
* Returns the DiscoItemsProvider responsible for providing the items related to this item. * Returns the DiscoItemsProvider responsible for providing the items related to this item.
...@@ -44,5 +69,7 @@ public interface DiscoServerItem extends DiscoItem { ...@@ -44,5 +69,7 @@ public interface DiscoServerItem extends DiscoItem {
* *
* @return the DiscoItemsProvider responsible for providing the items related to this item. * @return the DiscoItemsProvider responsible for providing the items related to this item.
*/ */
public abstract DiscoItemsProvider getDiscoItemsProvider(); public DiscoItemsProvider getDiscoItemsProvider() {
return itemsProvider;
}
} }
...@@ -25,6 +25,8 @@ import org.jivesoftware.openfire.handler.IQHandler; ...@@ -25,6 +25,8 @@ import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.rsm.ResultSet;
import org.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory; import org.jivesoftware.util.cache.CacheFactory;
import org.jivesoftware.util.lock.LockManager; import org.jivesoftware.util.lock.LockManager;
...@@ -59,6 +61,7 @@ import java.util.concurrent.locks.Lock; ...@@ -59,6 +61,7 @@ import java.util.concurrent.locks.Lock;
*/ */
public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListener { public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListener {
private static final String NAMESPACE_DISCO_INFO = "http://jabber.org/protocol/disco#info";
private Map<String, DiscoInfoProvider> entities = new HashMap<String, DiscoInfoProvider>(); private Map<String, DiscoInfoProvider> entities = new HashMap<String, DiscoInfoProvider>();
private Set<String> localServerFeatures = new CopyOnWriteArraySet<String>(); private Set<String> localServerFeatures = new CopyOnWriteArraySet<String>();
private Cache<String, Set<NodeID>> serverFeatures; private Cache<String, Set<NodeID>> serverFeatures;
...@@ -71,7 +74,8 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene ...@@ -71,7 +74,8 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene
public IQDiscoInfoHandler() { public IQDiscoInfoHandler() {
super("XMPP Disco Info Handler"); super("XMPP Disco Info Handler");
info = new IQHandlerInfo("query", "http://jabber.org/protocol/disco#info"); info = new IQHandlerInfo("query", NAMESPACE_DISCO_INFO);
addServerFeature(NAMESPACE_DISCO_INFO);
// Initialize the user identity and features collections (optimization to avoid creating // Initialize the user identity and features collections (optimization to avoid creating
// the same objects for each response) // the same objects for each response)
Element userIdentity = DocumentHelper.createElement("identity"); Element userIdentity = DocumentHelper.createElement("identity");
...@@ -82,7 +86,7 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene ...@@ -82,7 +86,7 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene
userIdentity.addAttribute("category", "account"); userIdentity.addAttribute("category", "account");
userIdentity.addAttribute("type", "registered"); userIdentity.addAttribute("type", "registered");
registeredUserIdentities.add(userIdentity); registeredUserIdentities.add(userIdentity);
userFeatures.add("http://jabber.org/protocol/disco#info"); userFeatures.add(NAMESPACE_DISCO_INFO);
} }
public IQHandlerInfo getInfo() { public IQHandlerInfo getInfo() {
...@@ -99,8 +103,10 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene ...@@ -99,8 +103,10 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene
// We consider the host of the recipient JID of the packet as the entity. It's the // We consider the host of the recipient JID of the packet as the entity. It's the
// DiscoInfoProvider responsibility to provide information about the JID's name together // DiscoInfoProvider responsibility to provide information about the JID's name together
// with any possible requested node. // with any possible requested node.
DiscoInfoProvider infoProvider = getProvider(packet.getTo() == null ? final String toAddress = packet.getTo() == null ? XMPPServer
XMPPServer.getInstance().getServerInfo().getName() : packet.getTo().getDomain()); .getInstance().getServerInfo().getName() : packet.getTo()
.getDomain();
DiscoInfoProvider infoProvider = getProvider(toAddress);
if (infoProvider != null) { if (infoProvider != null) {
// Get the JID's name // Get the JID's name
String name = packet.getTo() == null ? null : packet.getTo().getNode(); String name = packet.getTo() == null ? null : packet.getTo().getNode();
...@@ -119,17 +125,49 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene ...@@ -119,17 +125,49 @@ public class IQDiscoInfoHandler extends IQHandler implements ClusterEventListene
// Add to the reply all the identities provided by the DiscoInfoProvider // Add to the reply all the identities provided by the DiscoInfoProvider
Element identity; Element identity;
Iterator identities = infoProvider.getIdentities(name, node, packet.getFrom()); Iterator<Element> identities = infoProvider.getIdentities(name, node,
packet.getFrom());
while (identities.hasNext()) { while (identities.hasNext()) {
identity = (Element)identities.next(); identity = identities.next();
identity.setQName(new QName(identity.getName(), queryElement.getNamespace())); identity.setQName(new QName(identity.getName(), queryElement.getNamespace()));
queryElement.add((Element)identity.clone()); queryElement.add((Element)identity.clone());
} }
// Add to the reply all the features provided by the DiscoInfoProvider // Add to the reply all the features provided by the DiscoInfoProvider
Iterator features = infoProvider.getFeatures(name, node, packet.getFrom()); Iterator<String> features = infoProvider.getFeatures(name, node, packet.getFrom());
boolean hasDiscoInfoFeature = false;
boolean hasDiscoItemsFeature = false;
boolean hasResultSetManagementFeature = false;
while (features.hasNext()) { while (features.hasNext()) {
queryElement.addElement("feature").addAttribute("var", (String)features.next()); final String feature = features.next();
queryElement.addElement("feature").addAttribute("var", feature);
if (feature.equals(NAMESPACE_DISCO_INFO)) {
hasDiscoInfoFeature = true;
} else if (feature.equals("http://jabber.org/protocol/disco#items")) {
hasDiscoItemsFeature = true;
} else if (feature.equals(ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT)) {
hasResultSetManagementFeature = true;
}
}
if (hasDiscoItemsFeature && !hasResultSetManagementFeature) {
// IQDiscoItemsHandler provides result set management
// support.
queryElement.addElement("feature").addAttribute("var",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);
}
if (!hasDiscoInfoFeature) {
// XEP-0030 requires that every entity that supports service
// discovery broadcasts the disco#info feature.
Log.info("InfoProvider for '" + toAddress
+ "' does not return the disco#info feature. "
+ "Openfire will add the feature on the entities behalf, "
+ "but the entities response to Service Discovery request should be modified.");
queryElement.addElement("feature").addAttribute("var",
NAMESPACE_DISCO_INFO);
} }
// Add to the reply the extended info (XDataForm) provided by the DiscoInfoProvider // Add to the reply the extended info (XDataForm) provided by the DiscoInfoProvider
......
...@@ -31,9 +31,12 @@ import org.jivesoftware.util.cache.Cache; ...@@ -31,9 +31,12 @@ import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory; import org.jivesoftware.util.cache.CacheFactory;
import org.jivesoftware.util.cache.ExternalizableUtil; import org.jivesoftware.util.cache.ExternalizableUtil;
import org.jivesoftware.util.lock.LockManager; import org.jivesoftware.util.lock.LockManager;
import org.jivesoftware.util.rsm.ResultSet;
import org.jivesoftware.util.rsm.ResultSetImpl;
import org.xmpp.packet.IQ; import org.xmpp.packet.IQ;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError; import org.xmpp.packet.PacketError;
import org.xmpp.packet.PacketError.Condition;
import java.io.Externalizable; import java.io.Externalizable;
import java.io.IOException; import java.io.IOException;
...@@ -67,16 +70,18 @@ import java.util.concurrent.locks.Lock; ...@@ -67,16 +70,18 @@ import java.util.concurrent.locks.Lock;
*/ */
public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProvider, ClusterEventListener { public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProvider, ClusterEventListener {
public static final String NAMESPACE_DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
private Map<String,DiscoItemsProvider> entities = new HashMap<String,DiscoItemsProvider>(); private Map<String,DiscoItemsProvider> entities = new HashMap<String,DiscoItemsProvider>();
private Map<String, Element> localServerItems = new HashMap<String, Element>(); private Map<String, Element> localServerItems = new HashMap<String, Element>();
private Cache<String, ClusteredServerItem> serverItems; private Cache<String, ClusteredServerItem> serverItems;
private Map<String, DiscoItemsProvider> serverNodeProviders = new ConcurrentHashMap<String, DiscoItemsProvider>(); private Map<String, DiscoItemsProvider> serverNodeProviders
= new ConcurrentHashMap<String, DiscoItemsProvider>();
private IQHandlerInfo info; private IQHandlerInfo info;
private IQDiscoInfoHandler infoHandler; private IQDiscoInfoHandler infoHandler;
public IQDiscoItemsHandler() { public IQDiscoItemsHandler() {
super("XMPP Disco Items Handler"); super("XMPP Disco Items Handler");
info = new IQHandlerInfo("query", "http://jabber.org/protocol/disco#items"); info = new IQHandlerInfo("query", NAMESPACE_DISCO_ITEMS);
} }
public IQHandlerInfo getInfo() { public IQHandlerInfo getInfo() {
...@@ -113,19 +118,72 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -113,19 +118,72 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
String node = iq.attributeValue("node"); String node = iq.attributeValue("node");
// Check if we have items associated with the requested name and node // Check if we have items associated with the requested name and node
Iterator<Element> itemsItr = itemsProvider.getItems(name, node, packet.getFrom()); Iterator<DiscoItem> itemsItr = itemsProvider.getItems(name, node,
packet.getFrom());
if (itemsItr != null) { if (itemsItr != null) {
reply.setChildElement(iq.createCopy()); reply.setChildElement(iq.createCopy());
Element queryElement = reply.getChildElement(); Element queryElement = reply.getChildElement();
// Add to the reply all the items provided by the DiscoItemsProvider // See if the requesting entity would like to apply 'result set
// management'
final Element rsmElement = packet.getChildElement().element(
QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
// apply RSM only if the element exists, and the (total) results
// set is not empty.
final boolean applyRSM = rsmElement != null
&& itemsItr.hasNext();
if (applyRSM) {
if (!ResultSet.isValidRSMRequest(rsmElement))
{
reply.setError(Condition.bad_request);
return reply;
}
// Calculate which results to include.
final List<DiscoItem> rsmResults;
final List<DiscoItem> allItems = new ArrayList<DiscoItem>();
while (itemsItr.hasNext()) {
allItems.add(itemsItr.next());
}
final ResultSet<DiscoItem> rs = new ResultSetImpl<DiscoItem>(
allItems);
try {
rsmResults = rs.applyRSMDirectives(rsmElement);
} catch (NullPointerException e) {
final IQ itemNotFound = IQ.createResultIQ(packet);
itemNotFound.setError(Condition.item_not_found);
return itemNotFound;
}
// add the applicable results to the IQ-result
for (DiscoItem item : rsmResults) {
final Element resultElement = item.getElement();
resultElement.setQName(new QName(resultElement
.getName(), queryElement.getNamespace()));
queryElement.add(resultElement.createCopy());
}
// overwrite the 'set' element.
queryElement.remove(queryElement.element(
QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT)));
queryElement.add(rs.generateSetElementFromResults(rsmResults));
} else {
// don't apply RSM:
// Add to the reply all the items provided by the
// DiscoItemsProvider
Element item; Element item;
while (itemsItr.hasNext()) { while (itemsItr.hasNext()) {
item = itemsItr.next(); item = itemsItr.next().getElement();
item.setQName(new QName(item.getName(), queryElement.getNamespace())); item.setQName(new QName(item.getName(), queryElement
.getNamespace()));
queryElement.add(item.createCopy()); queryElement.add(item.createCopy());
} }
} }
}
else { else {
// If the DiscoItemsProvider has no items for the requested name and node // If the DiscoItemsProvider has no items for the requested name and node
// then answer a not found error // then answer a not found error
...@@ -193,10 +251,10 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -193,10 +251,10 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
while (items.hasNext()) { while (items.hasNext()) {
discoItem = items.next(); discoItem = items.next();
// Add the element to the list of items related to the server // Add the element to the list of items related to the server
addComponentItem(discoItem.getJID(), discoItem.getNode(), discoItem.getName()); addComponentItem(discoItem.getJID().toString(), discoItem.getNode(), discoItem.getName());
// Add the new item as a valid entity that could receive info and items disco requests // Add the new item as a valid entity that could receive info and items disco requests
String host = new JID(discoItem.getJID()).getDomain(); String host = discoItem.getJID().getDomain();
infoHandler.setProvider(host, discoItem.getDiscoInfoProvider()); infoHandler.setProvider(host, discoItem.getDiscoInfoProvider());
setProvider(host, discoItem.getDiscoItemsProvider()); setProvider(host, discoItem.getDiscoItemsProvider());
} }
...@@ -217,10 +275,10 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -217,10 +275,10 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
while (items.hasNext()) { while (items.hasNext()) {
discoItem = items.next(); discoItem = items.next();
// Remove the item from the server items list // Remove the item from the server items list
removeComponentItem(discoItem.getJID()); removeComponentItem(discoItem.getJID().toString());
// Remove the item as a valid entity that could receive info and items disco requests // Remove the item as a valid entity that could receive info and items disco requests
String host = new JID(discoItem.getJID()).getDomain(); String host = discoItem.getJID().getDomain();
infoHandler.removeProvider(host); infoHandler.removeProvider(host);
removeProvider(host); removeProvider(host);
} }
...@@ -343,7 +401,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -343,7 +401,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
public Iterator<String> getFeatures() { public Iterator<String> getFeatures() {
List<String> features = new ArrayList<String>(); List<String> features = new ArrayList<String>();
features.add("http://jabber.org/protocol/disco#items"); features.add(NAMESPACE_DISCO_ITEMS);
// TODO Comment out this line when publishing of client items is implemented // TODO Comment out this line when publishing of client items is implemented
//features.add("http://jabber.org/protocol/disco#publish"); //features.add("http://jabber.org/protocol/disco#publish");
return features.iterator(); return features.iterator();
...@@ -418,7 +476,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -418,7 +476,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
private DiscoItemsProvider getServerItemsProvider() { private DiscoItemsProvider getServerItemsProvider() {
return new DiscoItemsProvider() { return new DiscoItemsProvider() {
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
if (node != null) { if (node != null) {
// Check if there is a provider for the requested node // Check if there is a provider for the requested node
if (serverNodeProviders.get(node) != null) { if (serverNodeProviders.get(node) != null) {
...@@ -433,8 +491,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -433,8 +491,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
} }
return answer.iterator(); return answer.iterator();
} }
else { final List<DiscoItem> answer = new ArrayList<DiscoItem>();
List<Element> answer = new ArrayList<Element>();
try { try {
User user = UserManager.getInstance().getUser(name); User user = UserManager.getInstance().getUser(name);
RosterItem item = user.getRoster().getRosterItem(senderJID); RosterItem item = user.getRoster().getRosterItem(senderJID);
...@@ -443,9 +500,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -443,9 +500,7 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
if (item.getSubStatus() == RosterItem.SUB_FROM || if (item.getSubStatus() == RosterItem.SUB_FROM ||
item.getSubStatus() == RosterItem.SUB_BOTH) { item.getSubStatus() == RosterItem.SUB_BOTH) {
for (Session session : SessionManager.getInstance().getSessions(name)) { for (Session session : SessionManager.getInstance().getSessions(name)) {
Element element = DocumentHelper.createElement("item"); answer.add(new DiscoItem(session.getAddress(), null, null, null));
element.addAttribute("jid", session.getAddress().toString());
answer.add(element);
} }
} }
return answer.iterator(); return answer.iterator();
...@@ -454,7 +509,6 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv ...@@ -454,7 +509,6 @@ public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProv
return answer.iterator(); return answer.iterator();
} }
} }
}
}; };
} }
......
...@@ -11,12 +11,26 @@ ...@@ -11,12 +11,26 @@
package org.jivesoftware.openfire.filetransfer.proxy; package org.jivesoftware.openfire.filetransfer.proxy;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.dom4j.DocumentHelper; import org.dom4j.DocumentHelper;
import org.dom4j.Element; import org.dom4j.Element;
import org.jivesoftware.openfire.*; import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.PacketException;
import org.jivesoftware.openfire.PacketRouter;
import org.jivesoftware.openfire.RoutableChannelHandler;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.disco.DiscoInfoProvider; import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.openfire.disco.ServerItemsProvider;
...@@ -31,10 +45,6 @@ import org.xmpp.packet.JID; ...@@ -31,10 +45,6 @@ import org.xmpp.packet.JID;
import org.xmpp.packet.Packet; import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError; import org.xmpp.packet.PacketError;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
/** /**
* Manages the transfering of files between two remote entities on the jabber network. * Manages the transfering of files between two remote entities on the jabber network.
* This class acts independtly as a Jabber component from the rest of the server, according to * This class acts independtly as a Jabber component from the rest of the server, according to
...@@ -284,31 +294,8 @@ public class FileTransferProxy extends BasicModule ...@@ -284,31 +294,8 @@ public class FileTransferProxy extends BasicModule
return items.iterator(); return items.iterator();
} }
items.add(new DiscoServerItem() { items.add(new DiscoServerItem(new JID(getServiceDomain()),
public String getJID() { "Socks 5 Bytestreams Proxy", null, null, this, this));
return getServiceDomain();
}
public String getName() {
return "Socks 5 Bytestreams Proxy";
}
public String getAction() {
return null;
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return FileTransferProxy.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return FileTransferProxy.this;
}
});
return items.iterator(); return items.iterator();
} }
...@@ -338,9 +325,9 @@ public class FileTransferProxy extends BasicModule ...@@ -338,9 +325,9 @@ public class FileTransferProxy extends BasicModule
return true; return true;
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
// A proxy server has no items // A proxy server has no items
return new ArrayList<Element>().iterator(); return new ArrayList<DiscoItem>().iterator();
} }
public void process(Packet packet) throws UnauthorizedException, PacketException { public void process(Packet packet) throws UnauthorizedException, PacketException {
......
...@@ -159,19 +159,15 @@ public class IQOfflineMessagesHandler extends IQHandler implements ServerFeature ...@@ -159,19 +159,15 @@ public class IQOfflineMessagesHandler extends IQHandler implements ServerFeature
return NAMESPACE.equals(node) && userManager.isRegisteredUser(senderJID.getNode()); return NAMESPACE.equals(node) && userManager.isRegisteredUser(senderJID.getNode());
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
// Mark that offline messages shouldn't be sent when the user becomes available // Mark that offline messages shouldn't be sent when the user becomes available
stopOfflineFlooding(senderJID); stopOfflineFlooding(senderJID);
List<Element> answer = new ArrayList<Element>(); List<DiscoItem> answer = new ArrayList<DiscoItem>();
Element item;
for (OfflineMessage offlineMessage : messageStore.getMessages(senderJID.getNode(), false)) { for (OfflineMessage offlineMessage : messageStore.getMessages(senderJID.getNode(), false)) {
item = DocumentHelper.createElement("item"); final DiscoItem item;
item.addAttribute("jid", senderJID.toBareJID());
item.addAttribute("name", offlineMessage.getFrom().toString());
synchronized (dateFormat) { synchronized (dateFormat) {
item.addAttribute("node", dateFormat.format(offlineMessage.getCreationDate())); item = new DiscoItem(senderJID, offlineMessage.getFrom().toString(), dateFormat.format(offlineMessage.getCreationDate()), null);
} }
answer.add(item); answer.add(item);
} }
......
...@@ -17,6 +17,7 @@ import org.jivesoftware.openfire.*; ...@@ -17,6 +17,7 @@ import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.disco.DiscoInfoProvider; import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.openfire.disco.ServerItemsProvider;
...@@ -98,14 +99,14 @@ public class MediaProxyService extends BasicModule ...@@ -98,14 +99,14 @@ public class MediaProxyService extends BasicModule
XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this); XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this);
} else { } else {
if (echo != null) echo.cancel(); if (echo != null) echo.cancel();
XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress().toString()); XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress());
} }
} }
public void stop() { public void stop() {
super.stop(); super.stop();
mediaProxy.stopProxy(); mediaProxy.stopProxy();
XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress().toString()); XMPPServer.getInstance().getIQDiscoItemsHandler().removeComponentItem(getAddress());
routingTable.removeComponentRoute(getAddress()); routingTable.removeComponentRoute(getAddress());
if (echo != null) echo.cancel(); if (echo != null) echo.cancel();
} }
...@@ -117,9 +118,9 @@ public class MediaProxyService extends BasicModule ...@@ -117,9 +118,9 @@ public class MediaProxyService extends BasicModule
return serviceName; return serviceName;
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
// A proxy server has no items // A proxy server has no items
return new ArrayList<Element>().iterator(); return new ArrayList<DiscoItem>().iterator();
} }
public void process(Packet packet) throws UnauthorizedException, PacketException { public void process(Packet packet) throws UnauthorizedException, PacketException {
...@@ -293,31 +294,8 @@ public class MediaProxyService extends BasicModule ...@@ -293,31 +294,8 @@ public class MediaProxyService extends BasicModule
return items.iterator(); return items.iterator();
} }
items.add(new DiscoServerItem() { final DiscoServerItem item = new DiscoServerItem(new JID(getServiceDomain()), "Media Proxy Service", null, null, this, this);
public String getJID() { items.add(item);
return getServiceDomain();
}
public String getName() {
return "Media Proxy Service";
}
public String getAction() {
return null;
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return MediaProxyService.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return MediaProxyService.this;
}
});
return items.iterator(); return items.iterator();
} }
......
...@@ -19,6 +19,7 @@ import org.jivesoftware.openfire.cluster.ClusterEventListener; ...@@ -19,6 +19,7 @@ import org.jivesoftware.openfire.cluster.ClusterEventListener;
import org.jivesoftware.openfire.cluster.ClusterManager; import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.disco.DiscoInfoProvider; import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.openfire.disco.ServerItemsProvider;
...@@ -960,37 +961,19 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha ...@@ -960,37 +961,19 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
return null; return null;
} }
ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>(); ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>();
items.add(new DiscoServerItem() { final String name;
public String getJID() {
return getServiceDomain();
}
public String getName() {
// Check if there is a system property that overrides the default value // Check if there is a system property that overrides the default value
String serviceName = JiveGlobals.getProperty("muc.service-name"); String serviceName = JiveGlobals.getProperty("muc.service-name");
if (serviceName != null && serviceName.trim().length() > 0) { if (serviceName != null && serviceName.trim().length() > 0) {
return serviceName; name = serviceName;
} }
else {
// Return the default service name based on the current locale // Return the default service name based on the current locale
return LocaleUtils.getLocalizedString("muc.service-name"); name = LocaleUtils.getLocalizedString("muc.service-name");
} }
public String getAction() { final DiscoServerItem item = new DiscoServerItem(new JID(getServiceDomain()), name, null, null, this, this);
return null; items.add(item);
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return MultiUserChatServerImpl.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return MultiUserChatServerImpl.this;
}
});
return items.iterator(); return items.iterator();
} }
...@@ -1150,22 +1133,17 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha ...@@ -1150,22 +1133,17 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
return false; return false;
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
// Check if the service is disabled. Info is not available when disabled. // Check if the service is disabled. Info is not available when disabled.
if (!isServiceEnabled()) { if (!isServiceEnabled()) {
return null; return null;
} }
List<Element> answer = new ArrayList<Element>(); List<DiscoItem> answer = new ArrayList<DiscoItem>();
if (name == null && node == null) { if (name == null && node == null) {
Element item;
// Answer all the public rooms as items // Answer all the public rooms as items
for (MUCRoom room : rooms.values()) { for (MUCRoom room : rooms.values()) {
if (canDiscoverRoom(room)) { if (canDiscoverRoom(room)) {
item = DocumentHelper.createElement("item"); answer.add(new DiscoItem(room.getRole().getRoleAddress(), room.getNaturalLanguageName(), null, null));
item.addAttribute("jid", room.getRole().getRoleAddress().toString());
item.addAttribute("name", room.getNaturalLanguageName());
answer.add(item);
} }
} }
} }
...@@ -1173,13 +1151,9 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha ...@@ -1173,13 +1151,9 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
// Answer the room occupants as items if that info is publicly available // Answer the room occupants as items if that info is publicly available
MUCRoom room = getChatRoom(name); MUCRoom room = getChatRoom(name);
if (room != null && canDiscoverRoom(room)) { if (room != null && canDiscoverRoom(room)) {
Element item;
for (MUCRole role : room.getOccupants()) { for (MUCRole role : room.getOccupants()) {
// TODO Should we filter occupants that are invisible (presence is not broadcasted)? // TODO Should we filter occupants that are invisible (presence is not broadcasted)?
item = DocumentHelper.createElement("item"); answer.add(new DiscoItem(role.getRoleAddress(), null, null, null));
item.addAttribute("jid", role.getRoleAddress().toString());
answer.add(item);
} }
} }
} }
......
...@@ -22,6 +22,7 @@ import org.jivesoftware.openfire.cluster.ClusterManager; ...@@ -22,6 +22,7 @@ import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.component.InternalComponentManager; import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.disco.DiscoInfoProvider; import org.jivesoftware.openfire.disco.DiscoInfoProvider;
import org.jivesoftware.openfire.disco.DiscoItem;
import org.jivesoftware.openfire.disco.DiscoItemsProvider; import org.jivesoftware.openfire.disco.DiscoItemsProvider;
import org.jivesoftware.openfire.disco.DiscoServerItem; import org.jivesoftware.openfire.disco.DiscoServerItem;
import org.jivesoftware.openfire.disco.ServerItemsProvider; import org.jivesoftware.openfire.disco.ServerItemsProvider;
...@@ -468,32 +469,8 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di ...@@ -468,32 +469,8 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di
public Iterator<DiscoServerItem> getItems() { public Iterator<DiscoServerItem> getItems() {
ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>(); ArrayList<DiscoServerItem> items = new ArrayList<DiscoServerItem>();
final DiscoServerItem item = new DiscoServerItem(new JID(getServiceDomain()), "Publish-Subscribe service", null, null, this, this);
items.add(new DiscoServerItem() { items.add(item);
public String getJID() {
return getServiceDomain();
}
public String getName() {
return "Publish-Subscribe service";
}
public String getAction() {
return null;
}
public String getNode() {
return null;
}
public DiscoInfoProvider getDiscoInfoProvider() {
return PubSubModule.this;
}
public DiscoItemsProvider getDiscoItemsProvider() {
return PubSubModule.this;
}
});
return items.iterator(); return items.iterator();
} }
...@@ -636,18 +613,14 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di ...@@ -636,18 +613,14 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di
return false; return false;
} }
public Iterator<Element> getItems(String name, String node, JID senderJID) { public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
List<Element> answer = new ArrayList<Element>(); List<DiscoItem> answer = new ArrayList<DiscoItem>();
String serviceDomain = getServiceDomain(); String serviceDomain = getServiceDomain();
if (name == null && node == null) { if (name == null && node == null) {
Element item;
// Answer all first level nodes // Answer all first level nodes
for (Node pubNode : rootCollectionNode.getNodes()) { for (Node pubNode : rootCollectionNode.getNodes()) {
if (canDiscoverNode(pubNode)) { if (canDiscoverNode(pubNode)) {
item = DocumentHelper.createElement("item"); final DiscoItem item = new DiscoItem(new JID(serviceDomain), pubNode.getName(), pubNode.getNodeID(), null);
item.addAttribute("jid", serviceDomain);
item.addAttribute("node", pubNode.getNodeID());
item.addAttribute("name", pubNode.getName());
answer.add(item); answer.add(item);
} }
} }
...@@ -656,25 +629,18 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di ...@@ -656,25 +629,18 @@ public class PubSubModule extends BasicModule implements ServerItemsProvider, Di
Node pubNode = getNode(node); Node pubNode = getNode(node);
if (pubNode != null && canDiscoverNode(pubNode)) { if (pubNode != null && canDiscoverNode(pubNode)) {
if (pubNode.isCollectionNode()) { if (pubNode.isCollectionNode()) {
Element item;
// Answer all nested nodes as items // Answer all nested nodes as items
for (Node nestedNode : pubNode.getNodes()) { for (Node nestedNode : pubNode.getNodes()) {
if (canDiscoverNode(nestedNode)) { if (canDiscoverNode(nestedNode)) {
item = DocumentHelper.createElement("item"); final DiscoItem item = new DiscoItem(new JID(serviceDomain), nestedNode.getName(), nestedNode.getNodeID(), null);
item.addAttribute("jid", serviceDomain);
item.addAttribute("node", nestedNode.getNodeID());
item.addAttribute("name", nestedNode.getName());
answer.add(item); answer.add(item);
} }
} }
} }
else { else {
// This is a leaf node so answer the published items which exist on the service // This is a leaf node so answer the published items which exist on the service
Element item;
for (PublishedItem publishedItem : pubNode.getPublishedItems()) { for (PublishedItem publishedItem : pubNode.getPublishedItems()) {
item = DocumentHelper.createElement("item"); final DiscoItem item = new DiscoItem(new JID(serviceDomain), publishedItem.getID(), null, null);
item.addAttribute("jid", serviceDomain);
item.addAttribute("name", publishedItem.getID());
answer.add(item); answer.add(item);
} }
} }
......
...@@ -20,6 +20,7 @@ import org.jivesoftware.util.Log; ...@@ -20,6 +20,7 @@ import org.jivesoftware.util.Log;
import org.jivesoftware.util.cache.CacheSizes; import org.jivesoftware.util.cache.CacheSizes;
import org.jivesoftware.util.cache.Cacheable; import org.jivesoftware.util.cache.Cacheable;
import org.jivesoftware.util.cache.ExternalizableUtil; import org.jivesoftware.util.cache.ExternalizableUtil;
import org.jivesoftware.util.rsm.Result;
import java.io.Externalizable; import java.io.Externalizable;
import java.io.IOException; import java.io.IOException;
...@@ -42,7 +43,7 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -42,7 +43,7 @@ import java.util.concurrent.ConcurrentHashMap;
* *
* @author Matt Tucker * @author Matt Tucker
*/ */
public class User implements Cacheable, Externalizable { public class User implements Cacheable, Externalizable, Result {
private static final String LOAD_PROPERTIES = private static final String LOAD_PROPERTIES =
"SELECT name, propValue FROM jiveUserProp WHERE username=?"; "SELECT name, propValue FROM jiveUserProp WHERE username=?";
...@@ -529,4 +530,8 @@ public class User implements Cacheable, Externalizable { ...@@ -529,4 +530,8 @@ public class User implements Cacheable, Externalizable {
creationDate = new Date(ExternalizableUtil.getInstance().readLong(in)); creationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
modificationDate = new Date(ExternalizableUtil.getInstance().readLong(in)); modificationDate = new Date(ExternalizableUtil.getInstance().readLong(in));
} }
public String getUID() {
return username;
}
} }
package org.jivesoftware.util.rsm;
/**
* Elements from a result set as defined by XEP-0059 have certain
* characteristics. This interface defines these characteristics.
*
* Applying this interface to a class will allow you to use ResultSet operations
* on collections of your class. In other words: you are making collections of
* your class managable/navigable.
*
* @author Guus der Kinderen, guus@nimbuzz.com
* @see http://www.xmpp.org/extensions/xep-0059.html
*/
public interface Result {
/**
* Returns a unique identifier for this Result. Each element in a ResultSet
* must have a distinct UIDs.
*
* XEP-0059 says: <quote>(...) the UIDs are
* unique in the context of all possible members of the full result set.
* Each UID MAY be based on part of the content of its associated item (...)
* or on an internal table index. Another possible method is to serialize
* the XML of the item and then hash it to generate the UID. Note: The
* requesting entity MUST treat all UIDs as opaque.</quote>
*
* @return Unique ID of the Result
*/
public String getUID();
}
\ No newline at end of file
package org.jivesoftware.util.rsm;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
/**
* A result set representation as described in XEP-0059. A result set is a
* collection of objects that each have a unique identifier (UID).
*
* It's expected that some implementations will have the complete result set
* loaded into memory, whereas more complex implementations might keep
* references to partial sets only. This latter would have considerable
* advantages if the result set is extremely large, or if the operation to get
* all results in the set is expensive.
*
* @author Guus der Kinderen, guus@nimbuzz.com
*
* @param <E>
* Each result set should be a collection of instances of the exact
* same class. This class must implement the {@link Result}
* interface.
*/
public abstract class ResultSet<E extends Result> extends AbstractCollection<E> {
/**
* A list of field names that are valid in jabber:iq:search
*/
private final static Collection<String> validRequestFields = new ArrayList<String>();
static {
validRequestFields.add("max"); // required
validRequestFields.add("before");
validRequestFields.add("after");
validRequestFields.add("index");
}
/**
* The namespace that identifies Result Set Management functionality.
*/
public static final String NAMESPACE_RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
/**
* Returns a List of results starting with the first result after the
* provided result (the returned List is exclusive).
*
* The lenght of the list is equal to 'maxAmount', unless there are no more
* elements available (in which case the length of the result will be
* truncated).
*
* @param result
* The element that is right before the first element in the
* result.
* @param maxAmount
* The maximum number of elements to return.
* @return A List of elements the are exactly after the element that is
* provided as a parameter.
* @throws NullPointerException
* if the result does not exist in the result set.
*/
public List<E> getAfter(E result, int maxAmount) {
return getAfter(result.getUID(), maxAmount);
}
/**
* Returns a List of results starting with the first result after the result
* that's identified by the provided UID (the returned List is exclusive).
*
* The lenght of the list is equal to 'maxAmount', unless there are no more
* elements available (in which case the length of the result will be
* truncated).
*
* @param uid
* The UID of the element that is right before the first element
* in the result.
* @param maxAmount
* The maximum number of elements to return.
* @return A List of elements the are exactly after the element that is
* provided as a parameter.
* @throws NullPointerException
* if there is no result in the result set that matches the UID.
*/
public abstract List<E> getAfter(String uid, int maxAmount);
/**
* Returns a list of results ending with the element right before the
* provided result (the returned List is exclusive).
*
* At most 'maxAmount' elements are in the returned List, but the lenght of
* the result might be smaller if no more applicable elements are available.
*
* Note that the order of the result is equal to the order of the results of
* other methods of this class: the last element in the returned List
* immediately preceeds the element denoted by the 'result' parameter.
*
* @param result
* The element preceding the last element returned by this
* function.
* @param maxAmount
* The length of the List that is being returned.
* @return A List of elements that are exactly before the element that's
* provided as a parameter.
* @throws NullPointerException
* if the result does not exist in the result set.
*
*/
public List<E> getBefore(E result, int maxAmount) {
return getBefore(result.getUID(), maxAmount);
}
/**
* Returns a list of results ending with the element right before the
* element identified by the provided UID (the returned List is exclusive).
*
* At most 'maxAmount' elements are in the returned List, but the lenght of
* the result might be smaller if no more applicable elements are available.
*
* Note that the order of the result is equal to the order of the results of
* other methods of this class: the last element in the returned List
* immediately preceeds the element denoted by the 'result' parameter.
*
* @param uid
* The UID of the element preceding the last element returned by
* this function.
* @param maxAmount
* The length of the List that is being returned.
* @return A List of elements that are exactly before the element that's
* provided as a parameter.
* @throws NullPointerException
* if there is no result in the result set that matches the UID.
*/
public abstract List<E> getBefore(String uid, int maxAmount);
/**
* Returns the first elements from this result set.
*
* @param maxAmount
* the number of elements to return.
* @return the last 'maxAmount' elements of this result set.
*/
public abstract List<E> getFirst(int maxAmount);
/**
* Returns the last elements from this result set.
*
* @param maxAmount
* the number of elements to return.
* @return the last 'maxAmount' elements of this result set.
*/
public abstract List<E> getLast(int maxAmount);
/**
* Returns the element denoted by the index.
*
* @param index
* Index of the element to be returned
* @return the Element at 'index'.
*/
public abstract E get(int index);
/**
* Returns a list of results, starting with the result that's at the
* specified index. If the difference between the startIndex and the index
* of the last element in the entire resultset is smaller than the size
* supplied in the 'amount' parameter, the length of the returned list will
* be smaller than the 'amount' paramater. If the supplied index is equal
* to, or larger than the size of the result set, an empty List is returned.
*
* @param fromIndex
* The index of the first element to be returned.
* @param maxAmount
* The maximum number of elements to return.
* @return A list of elements starting with (inclusive) the element
* referenced by 'fromIndex'. An empty List if startIndex is equal
* to or bigger than the size of this entire result set.
*/
public abstract List<E> get(int fromIndex, int maxAmount);
/**
* Returns the UID of the object at the specified index in this result set.
*
* @param index
* The index of the UID to be returned.
* @return UID of the object on the specified index.
*/
public String getUID(int index) {
return get(index).getUID();
}
/**
* Returns the index in the full resultset of the element identified by the
* UID in te supplied argument.
*
* @param uid
* The UID of the element to search for
* @return The index of the element.
* @throws NullPointerException
* if there is no result in the result set that matches the UID.
*
*/
public abstract int indexOf(String uid);
/**
* Returns the index in the full resultset of the supplied argument.
*
* @param element
* The element to search for
* @return The index of the element.
*/
public int indexOf(E element) {
return indexOf(element.getUID());
}
/*
* (non-Javadoc)
*
* @see java.util.AbstractCollection#iterator()
*/
@Override
public Iterator<E> iterator() {
return new Itr();
}
/**
* Applies the 'result set management' directives to this result set, and
* returns a list of Results that matches the directives. Note that the
* orignal set is untouched. Instead, a new List is returned.
*
* @param rsmElement
* The XML element that contains the 'result set management'
* directives.
*/
public List<E> applyRSMDirectives(Element rsmElement) {
if (rsmElement == null || !isValidRSMRequest(rsmElement)) {
throw new IllegalArgumentException(
"The 'rsmElement' argument must be a valid, non-null RSM element.");
}
final int max = Integer.parseInt(rsmElement.element("max").getText());
if (max == 0) {
// this is a request for a resultset count.
return Collections.emptyList();
}
// optional elements
final Element afterElement = rsmElement.element("after");
final Element beforeElement = rsmElement.element("before");
final Element indexElement = rsmElement.element("index");
// Identify the pointer object in this set. This is the object before
// (or after) the first (respectivly last) element of the subset that
// should be returned. If no pointer is specified, the pointer is said
// to be before or after the first respectivly last element of the set.
String pointerUID = null; // by default, the pointer is before the
// first element of the set.
// by default, the search list is forward oriented.
boolean isForwardOriented = true;
if (afterElement != null) {
pointerUID = afterElement.getText();
} else if (beforeElement != null) {
pointerUID = beforeElement.getText();
isForwardOriented = false;
} else if (indexElement != null) {
final int index = Integer.parseInt(indexElement.getText());
if (index > 0) {
pointerUID = getUID(index - 1);
}
}
if (pointerUID != null && pointerUID.equals("")) {
pointerUID = null;
}
if (isForwardOriented) {
if (pointerUID == null) {
return getFirst(max);
}
return getAfter(pointerUID, max);
}
if (pointerUID == null) {
return getLast(max);
}
return getBefore(pointerUID, max);
}
/**
* Generates a Result Set Management 'set' element that describes the parto
* of the result set that was generated. You typically would use the List
* that was returned by {@link #applyRSMDirectives(Element)} as an argument
* to this method.
*
* @param returnedResults
* The subset of Results that is returned by the current query.
* @return An Element named 'set' that can be included in the result IQ
* stanza, which returns the subset of results.
*/
public Element generateSetElementFromResults(List<E> returnedResults) {
if (returnedResults == null) {
throw new IllegalArgumentException(
"Argument 'returnedResults' cannot be null.");
}
final Element setElement = DocumentHelper.createElement(QName.get(
"set", ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
// the size element contains the size of this entire result set.
setElement.addElement("count").setText(String.valueOf(size()));
// if the query wasn't a 'count only' query, add two more elements
if (returnedResults.size() > 0) {
final Element firstElement = setElement.addElement("first");
firstElement.addText(returnedResults.get(0).getUID());
firstElement.addAttribute("index", String
.valueOf(indexOf(returnedResults.get(0))));
setElement.addElement("last").addText(
returnedResults.get(returnedResults.size() - 1).getUID());
}
return setElement;
}
/**
* Checks if the Element that has been passed as an argument is a valid
* Result Set Management element, in a request context.
*
* @param rsmElement
* The Element to check.
* @return ''true'' if this is a valid RSM query representation, ''false''
* otherwise.
*/
// Dom4J doesn't do generics, sadly.
@SuppressWarnings("unchecked")
public static boolean isValidRSMRequest(Element rsmElement) {
if (rsmElement == null) {
throw new IllegalArgumentException(
"The argument 'rsmElement' cannot be null.");
}
if (!rsmElement.getName().equals("set")) {
// the name of the element must be "set".
return false;
}
if (!rsmElement.getNamespaceURI().equals(
NAMESPACE_RESULT_SET_MANAGEMENT)) {
// incorrect namespace
return false;
}
final Element maxElement = rsmElement.element("max");
if (maxElement == null) {
// The 'max' element in an RSM request must be available
return false;
}
final String sMax = maxElement.getText();
if (sMax == null || sMax.length() == 0) {
// max element must contain a value.
return false;
}
try {
if (Integer.parseInt(sMax) < 0) {
// must be a postive integer.
return false;
}
} catch (NumberFormatException e) {
// the value of 'max' must be an integer value.
return false;
}
List<Element> allElements = rsmElement.elements();
int optionalElements = 0;
for (Element element : allElements) {
final String name = element.getName();
if (!validRequestFields.contains(name)) {
// invalid element.
return false;
}
if (!name.equals("max")) {
optionalElements++;
}
if (optionalElements > 1) {
// only one optional element is allowed.
return false;
}
if (name.equals("index")) {
final String value = element.getText();
if (value == null || value.equals("")) {
// index elements must have a numberic value.
return false;
}
try {
if (Integer.parseInt(value) < 0) {
// index values must be positive.
return false;
}
} catch (NumberFormatException e) {
// index values must be numeric.
return false;
}
}
}
return true;
}
/**
* Basic Iterator implementation. Forward scrolling only. Does not support
* modification.
*
* @author Guus der Kinderen, guus@nimbuzz.com
*
*/
class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/*
* (non-Javadoc)
*
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext() {
return cursor != size();
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#next()
*/
public E next() {
return get(cursor++);
}
/*
* (non-Javadoc)
*
* @see java.util.Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
}
\ No newline at end of file
package org.jivesoftware.util.rsm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
/**
* A result set representation as described in XEP-0059. Note that this result
* 'set' actually makes use of a List implementations, as the Java Set
* definition disallows duplicate elements, while the List definition supplies
* most of the required indexing operations.
*
* This ResultSet implementation loads all all results from the set into memory,
* which might be undesirable for very large sets, or for sets where the
* retrieval of a result is an expensive operation. sets.
*
* As most methods are backed by the {@link List#subList(int, int)} method,
* non-structural changes in the returned lists are reflected in the ResultSet,
* and vice-versa.
*
* @author Guus der Kinderen, guus@nimbuzz.com
*
* @param <E>
* Each result set should be a collection of instances of the exact
* same class. This class must implement the {@link Result}
* interface.
* @see java.util.List#subList(int, int)
*
*/
/*
* TODO: do we want changes to the returned Lists of methods in this class be
* applied to the content of the ResultSet itself? Currently, because of the
* usage of java.util.List#subList(int, int), it does. I'm thinking a
* immodifiable solution would cause less problems. -Guus
*/
public class ResultSetImpl<E extends Result> extends ResultSet<E> {
/**
* A list of all results in this ResultSet
*/
public final List<E> resultList;
/**
* A mapping of the UIDs of all results in resultList, to the index of those
* entries in that list.
*/
public final Map<String, Integer> uidToIndex;
/**
* Creates a new Result Set instance, based on a collection of Result
* implementing objects. The collection should contain elements of the exact
* same class only, and cannot contain 'null' elements.
*
* The order that's being used in the new ResultSet instance is the same
* order in which {@link Collection#iterator()} iterates over the
* collection.
*
* Note that this constructor throws an IllegalArgumentException if the
* Collection that is provided contains Results that have duplicate UIDs.
*
* @param results
* The collection of Results that make up this result set.
*/
public ResultSetImpl(Collection<E> results) {
this(results, null);
}
/**
* Creates a new Result Set instance, based on a collection of Result
* implementing objects. The collection should contain elements of the exact
* same class only, and cannot contain 'null' elements.
*
* The order that's being used in the new ResultSet instance is defined by
* the supplied Comparator class.
*
* Note that this constructor throws an IllegalArgumentException if the
* Collection that is provided contains Results that have duplicate UIDs.
*
* @param results
* The collection of Results that make up this result set.
* @param comparator
* The Comparator that defines the order of the Results in this
* result set.
*/
public ResultSetImpl(Collection<E> results, Comparator<E> comparator) {
if (results == null) {
throw new NullPointerException("Argument 'results' cannot be null.");
}
final int size = results.size();
resultList = new ArrayList<E>(size);
uidToIndex = new Hashtable<String, Integer>(size);
// sort the collection, if need be.
List<E> sortedResults = null;
if (comparator != null) {
sortedResults = new ArrayList<E>(results);
Collections.sort(sortedResults, comparator);
}
int index = 0;
// iterate over either the sorted or unsorted collection
for (final E result : (sortedResults != null ? sortedResults : results)) {
if (result == null) {
throw new NullPointerException(
"The result set must not contain 'null' elements.");
}
final String uid = result.getUID();
if (uidToIndex.containsKey(uid)) {
throw new IllegalArgumentException(
"The result set can not contain elements that have the same UID.");
}
resultList.add(result);
uidToIndex.put(uid, Integer.valueOf(index));
index++;
}
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#size()
*/
@Override
public int size() {
return resultList.size();
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#getAfter(E, int)
*/
@Override
public List<E> getAfter(String uid, int maxAmount) {
if (uid == null || uid.length() == 0) {
throw new NullPointerException("Argument 'uid' cannot be null or an empty String.");
}
if (maxAmount < 1) {
throw new IllegalArgumentException(
"Argument 'maxAmount' must be a integer higher than zero.");
}
// the result of this method is exclusive 'result'
final int index = uidToIndex.get(uid) + 1;
return get(index, maxAmount);
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#getBefore(E, int)
*/
@Override
public List<E> getBefore(String uid, int maxAmount) {
if (uid == null || uid.length() == 0) {
throw new NullPointerException("Argument 'uid' cannot be null or an empty String.");
}
if (maxAmount < 1) {
throw new IllegalArgumentException(
"Argument 'maxAmount' must be a integer higher than zero.");
}
// the result of this method is exclusive 'result'
final int indexOfLastElement = uidToIndex.get(uid);
final int indexOfFirstElement = indexOfLastElement - maxAmount;
if (indexOfFirstElement < 0) {
return get(0, indexOfLastElement);
}
return get(indexOfFirstElement, maxAmount);
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#get(int)
*/
@Override
public E get(int index) {
return resultList.get(index);
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#getFirst(int)
*/
@Override
public List<E> getFirst(int maxAmount) {
if (maxAmount < 1) {
throw new IllegalArgumentException(
"Argument 'maxAmount' must be a integer higher than zero.");
}
return get(0, maxAmount);
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#getLast(int)
*/
@Override
public List<E> getLast(int maxAmount) {
if (maxAmount < 1) {
throw new IllegalArgumentException(
"Argument 'maxAmount' must be a integer higher than zero.");
}
final int indexOfFirstElement = size() - maxAmount;
if (indexOfFirstElement < 0) {
return get(0, maxAmount);
}
return get(indexOfFirstElement, maxAmount);
}
/*
* (non-Javadoc)
*
* @see com.buzzaa.xmpp.resultsetmanager.ResultSet#get(int, int)
*/
@Override
public List<E> get(int fromIndex, int maxAmount) {
if (fromIndex < 0) {
throw new IllegalArgumentException(
"Argument 'fromIndex' must be zero or higher.");
}
if (maxAmount < 1) {
throw new IllegalArgumentException(
"Argument 'maxAmount' must be a integer higher than zero.");
}
if (fromIndex >= size()) {
return new ArrayList<E>(0);
}
// calculate the last index to return, or return up to the end of last
// index if 'amount' surpasses the list length.
final int absoluteTo = fromIndex + maxAmount;
final int toIndex = (absoluteTo > size() ? size() : absoluteTo);
return resultList.subList(fromIndex, toIndex);
}
/*
* (non-Javadoc)
* @see org.jivesoftware.util.resultsetmanager.ResultSet#indexOf(java.lang.String)
*/
@Override
public int indexOf(String uid) {
return uidToIndex.get(uid).intValue();
}
}
...@@ -44,6 +44,11 @@ ...@@ -44,6 +44,11 @@
Search Plugin Changelog Search Plugin Changelog
</h1> </h1>
<p><b>1.4.2</b> -- XXX</p>
<ul>
<li>Implemented XEP-0059 "Result Set Management".</li>
</ul>
<p><b>1.4.1</b> -- June 20, 2007</p> <p><b>1.4.1</b> -- June 20, 2007</p>
<ul> <ul>
<li>Unescape username before returning search results to the client.</li> <li>Unescape username before returning search results to the client.</li>
......
...@@ -7,15 +7,24 @@ ...@@ -7,15 +7,24 @@
package org.jivesoftware.openfire.plugin; package org.jivesoftware.openfire.plugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import org.dom4j.DocumentHelper; import org.dom4j.DocumentHelper;
import org.dom4j.Element; import org.dom4j.Element;
import org.dom4j.QName; import org.dom4j.QName;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin; import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager; import org.jivesoftware.openfire.container.PluginManager;
...@@ -26,6 +35,14 @@ import org.jivesoftware.openfire.forms.spi.XFormFieldImpl; ...@@ -26,6 +35,14 @@ import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.openfire.user.User; import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.rsm.ResultSet;
import org.jivesoftware.util.rsm.ResultSetImpl;
import org.xmpp.component.Component; import org.xmpp.component.Component;
import org.xmpp.component.ComponentException; import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager; import org.xmpp.component.ComponentManager;
...@@ -33,52 +50,65 @@ import org.xmpp.component.ComponentManagerFactory; ...@@ -33,52 +50,65 @@ import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.IQ; import org.xmpp.packet.IQ;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
import org.xmpp.packet.Packet; import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError; import org.xmpp.packet.IQ.Type;
import org.xmpp.packet.PacketError.Condition;
import java.io.File;
import java.util.*;
/** /**
* Provides support for Jabber Search * Provides support for Jabber Search (<a
* (<a href="http://www.xmpp.org/extensions/xep-0055.html">XEP-0055</a>).<p> * href="http://www.xmpp.org/extensions/xep-0055.html">XEP-0055</a>).
* <p>
* *
* The basic functionality is to query an information repository * The basic functionality is to query an information repository regarding the
* regarding the possible search fields, to send a search query, * possible search fields, to send a search query, and to receive search
* and to receive search results. This implementation was primarily designed to use * results. This implementation was primarily designed to use <a
* <a href="http://www.xmpp.org/extensions/xep-0004.html">Data Forms</a>, but * href="http://www.xmpp.org/extensions/xep-0004.html">Data Forms</a>, but also
* also supports non-dataform searches. * supports non-dataform searches. <p/>
* <p/>
* *
* @author <a href="mailto:ryan@version2software.com">Ryan Graham</a> * @author <a href="mailto:ryan@version2software.com">Ryan Graham</a>
*/ */
public class SearchPlugin implements Component, Plugin, PropertyEventListener { public class SearchPlugin implements Component, Plugin, PropertyEventListener {
public static final String NAMESPACE_DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
public static final String NAMESPACE_DISCO_INFO = "http://jabber.org/protocol/disco#info";
public static final String NAMESPACE_JABBER_IQ_SEARCH = "jabber:iq:search";
public static final String SERVICENAME = "plugin.search.serviceName"; public static final String SERVICENAME = "plugin.search.serviceName";
public static final String SERVICEENABLED = "plugin.search.serviceEnabled"; public static final String SERVICEENABLED = "plugin.search.serviceEnabled";
public static final String EXCLUDEDFIELDS = "plugin.search.excludedFields"; public static final String EXCLUDEDFIELDS = "plugin.search.excludedFields";
private UserManager userManager; private final UserManager userManager;
private ComponentManager componentManager; private ComponentManager componentManager;
private PluginManager pluginManager; private PluginManager pluginManager;
private String serviceName; private String serviceName;
private boolean serviceEnabled; private boolean serviceEnabled;
private Collection<String> exculudedFields; private Collection<String> exculudedFields;
private static String serverName; private static String serverName;
private TreeMap<String, String> fieldLookup = new TreeMap<String, String>(new CaseInsensitiveComparator()); private final TreeMap<String, String> fieldLookup = new TreeMap<String, String>(
new CaseInsensitiveComparator());
private Map<String, String> reverseFieldLookup = new HashMap<String, String>(); private Map<String, String> reverseFieldLookup = new HashMap<String, String>();
public SearchPlugin() { public SearchPlugin() {
serviceName = JiveGlobals.getProperty(SERVICENAME, "search"); serviceName = JiveGlobals.getProperty(SERVICENAME, "search");
serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true); serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);
exculudedFields = StringUtils.stringToCollection(JiveGlobals.getProperty(EXCLUDEDFIELDS, "")); exculudedFields = StringUtils.stringToCollection(JiveGlobals
.getProperty(EXCLUDEDFIELDS, ""));
serverName = XMPPServer.getInstance().getServerInfo().getName(); serverName = XMPPServer.getInstance().getServerInfo().getName();
userManager = UserManager.getInstance(); userManager = UserManager.getInstance();
// Some clients, such as Miranda, are hard-coded to search specific fields, // Some clients, such as Miranda, are hard-coded to search specific
// so we map those fields to the fields that Openfire actually supports. // fields so we map those fields to the fields that Openfire actually supports.
fieldLookup.put("jid", "Username"); fieldLookup.put("jid", "Username");
fieldLookup.put("username", "Username"); fieldLookup.put("username", "Username");
fieldLookup.put("first", "Name"); fieldLookup.put("first", "Name");
...@@ -88,54 +118,92 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -88,54 +118,92 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
fieldLookup.put("email", "Email"); fieldLookup.put("email", "Email");
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#getName()
*/
public String getName() { public String getName() {
return pluginManager.getName(this); return pluginManager.getName(this);
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#getDescription()
*/
public String getDescription() { public String getDescription() {
return pluginManager.getDescription(this); return pluginManager.getDescription(this);
} }
/*
* (non-Javadoc)
*
* @see org.jivesoftware.openfire.container.Plugin#initializePlugin(org.jivesoftware.openfire.container.PluginManager,
* java.io.File)
*/
public void initializePlugin(PluginManager manager, File pluginDirectory) { public void initializePlugin(PluginManager manager, File pluginDirectory) {
pluginManager = manager; pluginManager = manager;
componentManager = ComponentManagerFactory.getComponentManager(); componentManager = ComponentManagerFactory.getComponentManager();
try { try {
componentManager.addComponent(serviceName, this); componentManager.addComponent(serviceName, this);
} } catch (ComponentException e) {
catch (ComponentException e) {
componentManager.getLog().error(e); componentManager.getLog().error(e);
} }
PropertyEventDispatcher.addListener(this); PropertyEventDispatcher.addListener(this);
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#initialize(org.xmpp.packet.JID,
* org.xmpp.component.ComponentManager)
*/
public void initialize(JID jid, ComponentManager componentManager) { public void initialize(JID jid, ComponentManager componentManager) {
// intentionally left blank. See initializePlugin()
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#start()
*/
public void start() { public void start() {
// intentionally left blank. See initializePlugin()
} }
/*
* (non-Javadoc)
*
* @see org.jivesoftware.openfire.container.Plugin#destroyPlugin()
*/
public void destroyPlugin() { public void destroyPlugin() {
PropertyEventDispatcher.removeListener(this); PropertyEventDispatcher.removeListener(this);
pluginManager = null; pluginManager = null;
try { try {
componentManager.removeComponent(serviceName); componentManager.removeComponent(serviceName);
componentManager = null; componentManager = null;
} } catch (Exception e) {
catch (Exception e) {
componentManager.getLog().error(e); componentManager.getLog().error(e);
} }
serviceName = null;
userManager = null;
exculudedFields = null;
serverName = null;
fieldLookup = null;
reverseFieldLookup = null;
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#shutdown()
*/
public void shutdown() { public void shutdown() {
// intentionally left blank. See destroyPlugin()
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#processPacket(org.xmpp.packet.Packet)
*/
public void processPacket(Packet p) { public void processPacket(Packet p) {
if (p instanceof IQ) { if (p instanceof IQ) {
IQ packet = (IQ) p; IQ packet = (IQ) p;
...@@ -146,45 +214,59 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -146,45 +214,59 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
namespace = childElement.getNamespaceURI(); namespace = childElement.getNamespaceURI();
} }
if ("jabber:iq:search".equals(namespace)) {
try { try {
IQ replyPacket = handleIQ(packet); if (NAMESPACE_JABBER_IQ_SEARCH.equals(namespace)) {
final IQ replyPacket = handleIQ(packet);
if (replyPacket != null) { if (replyPacket != null) {
componentManager.sendPacket(this, replyPacket); componentManager.sendPacket(this, replyPacket);
} }
} } else if (NAMESPACE_DISCO_INFO.equals(namespace)) {
catch (ComponentException e) { componentManager.sendPacket(this, handleDiscoInfo(packet));
componentManager.getLog().error(e); } else if (NAMESPACE_DISCO_ITEMS.equals(namespace)) {
} final IQ replyPacket = IQ.createResultIQ(packet);
} replyPacket.setChildElement("query", NAMESPACE_DISCO_ITEMS);
else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
try {
IQ replyPacket = IQ.createResultIQ(packet);
Element responseElement = replyPacket
.setChildElement("query", "http://jabber.org/protocol/disco#info");
responseElement.addElement("identity").addAttribute("category", "directory")
.addAttribute("type", "user")
.addAttribute("name", "User Search");
responseElement.addElement("feature").addAttribute("var", "jabber:iq:search");
componentManager.sendPacket(this, replyPacket); componentManager.sendPacket(this, replyPacket);
} }
catch (ComponentException e) { } catch (ComponentException e) {
componentManager.getLog().error(e); componentManager.getLog().error(e);
} }
} }
else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
try {
IQ replyPacket = IQ.createResultIQ(packet);
replyPacket.setChildElement("query", "http://jabber.org/protocol/disco#items");
componentManager.sendPacket(this, replyPacket);
}
catch (ComponentException e) {
componentManager.getLog().error(e);
} }
/**
* Creates a response specific to the search plugin to Disco#Info requests.
* @param iq The IQ stanza that contains the request.
* @return An IQ stanza, formulated as an answer to the received request.
*/
private static IQ handleDiscoInfo(IQ iq)
{
if (iq == null) {
throw new IllegalArgumentException("Argument 'iq' cannot be null.");
} }
if (!iq.getChildElement().getNamespaceURI().equals(
NAMESPACE_DISCO_INFO)
|| iq.getType() != Type.get) {
throw new IllegalArgumentException(
"This is not a valid disco#info request.");
} }
final IQ replyPacket = IQ.createResultIQ(iq);
final Element responseElement = replyPacket.setChildElement(
"query", NAMESPACE_DISCO_INFO);
responseElement.addElement("identity").addAttribute(
"category", "directory").addAttribute("type",
"user").addAttribute("name", "User Search");
responseElement.addElement("feature").addAttribute("var",
NAMESPACE_JABBER_IQ_SEARCH);
responseElement.addElement("feature").addAttribute("var",
NAMESPACE_DISCO_INFO);
responseElement.addElement("feature").addAttribute("var",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);
return replyPacket;
} }
private IQ handleIQ(IQ packet) { private IQ handleIQ(IQ packet) {
...@@ -192,67 +274,89 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -192,67 +274,89 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
return replyDisabled(packet); return replyDisabled(packet);
} }
if (IQ.Type.get.equals(packet.getType())) { switch (packet.getType()) {
case get:
return processGetPacket(packet); return processGetPacket(packet);
}
else if (IQ.Type.set.equals(packet.getType())) { case set:
return processSetPacket(packet); return processSetPacket(packet);
}
else if (IQ.Type.result.equals(packet.getType()) || IQ.Type.error.equals(packet.getType())) {
// Ignore
}
else {
// Unknown type was sent so return an error
IQ reply = new IQ(IQ.Type.error, packet.getID());
reply.setFrom(packet.getTo());
reply.setTo(packet.getFrom());
reply.setError(PacketError.Condition.bad_request);
return reply;
}
default:
// we can safely ignore 'error' and 'result' typed iq stanzas.
return null; return null;
} }
}
private IQ replyDisabled(IQ packet) { /**
* Constructs a IQ result stanza, based on the request stanza that is
* provided as an argument. The stanza tells the recipient that this service
* is currently unavailable.
*
* @param packet
* The request IQ stanza to which a result will be returned.
* @return A result stanza, telling the user that this service is
* unavailable.
*/
private static IQ replyDisabled(IQ packet) {
IQ replyPacket = IQ.createResultIQ(packet); IQ replyPacket = IQ.createResultIQ(packet);
Element reply = replyPacket.setChildElement("query", "jabber:iq:search"); Element reply = replyPacket
.setChildElement("query", NAMESPACE_JABBER_IQ_SEARCH);
XDataFormImpl unavailableForm = new XDataFormImpl(DataForm.TYPE_CANCEL); XDataFormImpl unavailableForm = new XDataFormImpl(DataForm.TYPE_CANCEL);
unavailableForm.setTitle(LocaleUtils.getLocalizedString("advance.user.search.title", "search")); unavailableForm.setTitle(LocaleUtils.getLocalizedString(
unavailableForm.addInstruction(LocaleUtils.getLocalizedString("search.service_unavailable", "search")); "advance.user.search.title", "search"));
unavailableForm.addInstruction(LocaleUtils.getLocalizedString(
"search.service_unavailable", "search"));
reply.add(unavailableForm.asXMLElement()); reply.add(unavailableForm.asXMLElement());
return replyPacket; return replyPacket;
} }
/**
* Processes an IQ stanza of type 'get', which in the context of 'Jabber
* Search' is a request for available search fields.
*
* @param packet
* An IQ stanza of type 'get'
* @return A result IQ stanza that contains the possbile search fields.
*/
private IQ processGetPacket(IQ packet) { private IQ processGetPacket(IQ packet) {
if (!packet.getType().equals(IQ.Type.get)) {
throw new IllegalArgumentException(
"This method only accepts 'get' typed IQ stanzas as an argument.");
}
IQ replyPacket = IQ.createResultIQ(packet); IQ replyPacket = IQ.createResultIQ(packet);
Element queryResult = DocumentHelper.createElement(QName.get("query", "jabber:iq:search")); Element queryResult = DocumentHelper.createElement(QName.get("query",
NAMESPACE_JABBER_IQ_SEARCH));
String instructions = LocaleUtils.getLocalizedString("advance.user.search.details", "search"); String instructions = LocaleUtils.getLocalizedString(
"advance.user.search.details", "search");
// non-data form // non-data form
queryResult.addElement("instructions").addText(instructions); queryResult.addElement("instructions").addText(instructions);
queryResult.addElement("first");
queryResult.addElement("last");
queryResult.addElement("nick");
queryResult.addElement("email");
XDataFormImpl searchForm = new XDataFormImpl(DataForm.TYPE_FORM); XDataFormImpl searchForm = new XDataFormImpl(DataForm.TYPE_FORM);
searchForm.setTitle(LocaleUtils.getLocalizedString("advance.user.search.title", "search")); searchForm.setTitle(LocaleUtils.getLocalizedString(
"advance.user.search.title", "search"));
searchForm.addInstruction(instructions); searchForm.addInstruction(instructions);
XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE"); XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
field.setType(FormField.TYPE_HIDDEN); field.setType(FormField.TYPE_HIDDEN);
field.addValue("jabber:iq:search"); field.addValue(NAMESPACE_JABBER_IQ_SEARCH);
searchForm.addField(field); searchForm.addField(field);
field = new XFormFieldImpl("search"); field = new XFormFieldImpl("search");
field.setType(FormField.TYPE_TEXT_SINGLE); field.setType(FormField.TYPE_TEXT_SINGLE);
field.setLabel(LocaleUtils.getLocalizedString("advance.user.search.search", "search")); field.setLabel(LocaleUtils.getLocalizedString(
"advance.user.search.search", "search"));
field.setRequired(true); field.setRequired(true);
searchForm.addField(field); searchForm.addField(field);
for (String searchField : getFilteredSearchFields()) { for (String searchField : getFilteredSearchFields()) {
// non-data form
queryResult.addElement(searchField);
field = new XFormFieldImpl(searchField); field = new XFormFieldImpl(searchField);
field.setType(FormField.TYPE_BOOLEAN); field.setType(FormField.TYPE_BOOLEAN);
field.addValue("1"); field.addValue("1");
...@@ -267,71 +371,180 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -267,71 +371,180 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
return replyPacket; return replyPacket;
} }
/**
* Processes an IQ stanza of type 'set', which in the context of 'Jabber
* Search' is a search request.
*
* @param packet
* An IQ stanza of type 'get'
* @return A result IQ stanza that contains the possbile search fields.
*/
private IQ processSetPacket(IQ packet) { private IQ processSetPacket(IQ packet) {
Set<User> users = new HashSet<User>(); if (!packet.getType().equals(IQ.Type.set)) {
throw new IllegalArgumentException(
"This method only accepts 'set' typed IQ stanzas as an argument.");
}
Element incomingForm = packet.getChildElement(); final Element incomingForm = packet.getChildElement();
boolean isDataFormQuery = (incomingForm.element(QName.get("x", "jabber:x:data")) != null); final boolean isDataFormQuery = (incomingForm.element(QName.get("x",
"jabber:x:data")) != null);
final Set<User> users = performSearch(incomingForm);
final Element rsmElement = incomingForm.element(QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
final boolean applyRSM = rsmElement != null && !users.isEmpty();
final IQ resultIQ;
if (applyRSM) {
// apply RSM
final List<User> rsmResults;
final ResultSet<User> rs = new ResultSetImpl<User>(users);
try
{
rsmResults = rs.applyRSM(rsmElement);
}
catch(NullPointerException e)
{
final IQ itemNotFound = IQ.createResultIQ(packet);
itemNotFound.setError(Condition.item_not_found);
return itemNotFound;
}
if (isDataFormQuery) {
resultIQ = replyDataFormResult(rsmResults, packet);
} else {
resultIQ = replyNonDataFormResult(rsmResults, packet);
}
// add the additional 'set' element.
final Element setElement = resultIQ.getChildElement().addElement(
QName.get("set", ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
setElement.addElement("count").setText(String.valueOf(rs.size()));
if (rsmResults.size() > 0) {
final Element firstElement = setElement.addElement("first");
firstElement.addText(rsmResults.get(0).getUID());
firstElement.addAttribute("index", String.valueOf(rs
.indexOf(rsmResults.get(0))));
setElement.addElement("last").addText(
rsmResults.get(rsmResults.size() - 1).getUID());
}
} else {
// don't apply RSM
if (isDataFormQuery) {
resultIQ = replyDataFormResult(users, packet);
} else {
resultIQ = replyNonDataFormResult(users, packet);
}
}
return resultIQ;
}
/**
* Performs a search based on form data, and returns the search results.
*
* @param incomingForm
* The form containing the search data
* @return A set of users that matches the search criteria.
*/
private Set<User> performSearch(Element incomingForm) {
Set<User> users = new HashSet<User>();
Hashtable<String, String> searchList = extractSearchQuery(incomingForm); Hashtable<String, String> searchList = extractSearchQuery(incomingForm);
Enumeration<String> searchIter = searchList.keys();
while (searchIter.hasMoreElements()) { for (Entry<String, String> entry : searchList.entrySet()) {
String field = searchIter.nextElement(); String field = entry.getKey();
String query = searchList.get(field); String query = entry.getValue();
Collection<User> foundUsers = new ArrayList<User>(); Collection<User> foundUsers = new ArrayList<User>();
if (userManager != null) { if (userManager != null) {
if (query.length() > 0 && !query.equals("jabber:iq:search")) { if (query.length() > 0 && !query.equals(NAMESPACE_JABBER_IQ_SEARCH)) {
foundUsers.addAll(userManager.findUsers(new HashSet<String>( foundUsers
.addAll(userManager.findUsers(new HashSet<String>(
Arrays.asList((field))), query)); Arrays.asList((field))), query));
} }
} } else {
else {
foundUsers.addAll(findUsers(field, query)); foundUsers.addAll(findUsers(field, query));
} }
//occasionally null a User is returned so filter them out // occasionally a null User is returned so filter them out
for (User user : foundUsers) { for (User user : foundUsers) {
if (user != null) { if (user != null) {
users.add(user); users.add(user);
} }
} }
} }
return users;
if (isDataFormQuery) {
return replyDataFormResult(users, packet);
}
else {
return replyNonDataFormResult(users, packet);
}
} }
/**
* This utilty method extracts the search query from the request. A query is
* defined as a set of key->value pairs, where the key denotes a search
* field, and the value contains the value that was filled out by the user
* for that field.
*
* The query can be specified in one of two ways. The first way is a query
* is formed is by filling out any of the the standard search fields. The
* other search method makes use of extended data forms. Search queries that
* are supplied to this {@link #extractSearchQuery(Element)} that make use
* of this last method get forwarded to
* {@link #extractExtendedSearchQuery(Element)}.
*
* @param incomingForm
* The form from which to extract the query
* @return The search query for a particular user search request.
*/
private Hashtable<String, String> extractSearchQuery(Element incomingForm) { private Hashtable<String, String> extractSearchQuery(Element incomingForm) {
Hashtable<String, String> searchList = new Hashtable<String, String>(); if (incomingForm.element(QName.get("x", "jabber:x:data")) != null) {
Element form = incomingForm.element(QName.get("x", "jabber:x:data")); // forward the request.
if (form == null) { return extractExtendedSearchQuery(incomingForm);
//since not all clients request which fields are available for searching }
//attempt to match submitted fields with available search fields
Iterator iter = incomingForm.elementIterator(); final Hashtable<String, String> searchList = new Hashtable<String, String>();
// since not all clients request which fields are available for
// searching attempt to match submitted fields with available search
// fields
Iterator<Element> iter = incomingForm.elementIterator();
while (iter.hasNext()) { while (iter.hasNext()) {
Element element = (Element) iter.next(); Element element = iter.next();
String name = element.getName(); String name = element.getName();
if (fieldLookup.containsKey(name)) { if (fieldLookup.containsKey(name)) {
//make best effort to map the fields submitted by // make best effort to map the fields submitted by
//the client to those that Openfire can search // the client to those that Openfire can search
reverseFieldLookup.put(fieldLookup.get(name), name); reverseFieldLookup.put(fieldLookup.get(name), name);
searchList.put(fieldLookup.get(name), element.getText()); searchList.put(fieldLookup.get(name), element.getText());
} }
} }
return searchList;
} }
else {
/**
* Extracts a search query from a data form that makes use of data forms to
* specify the search request. This 'extended' way of constructing a search
* request is documented in XEP-0055, chapter 3.
*
* @param incomingForm
* The form from which to extract the query
* @return The search query for a particular user search request.
* @see #extractSearchQuery(Element)
*/
private Hashtable<String, String> extractExtendedSearchQuery(
Element incomingForm) {
final Element dataform = incomingForm.element(QName.get("x",
"jabber:x:data"));
Hashtable<String, String> searchList = new Hashtable<String, String>();
List<String> searchFields = new ArrayList<String>(); List<String> searchFields = new ArrayList<String>();
String search = ""; String search = "";
Iterator fields = form.elementIterator("field"); Iterator<Element> fields = dataform.elementIterator("field");
while (fields.hasNext()) { while (fields.hasNext()) {
Element searchField = (Element) fields.next(); Element searchField = fields.next();
String field = searchField.attributeValue("var"); String field = searchField.attributeValue("var");
String value = ""; String value = "";
...@@ -340,8 +553,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -340,8 +553,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
if (field.equals("search")) { if (field.equals("search")) {
search = value; search = value;
} } else if (value.equals("1")) {
else if (value.equals("1")) {
searchFields.add(field); searchFields.add(field);
} }
} }
...@@ -349,19 +561,21 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -349,19 +561,21 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
for (String field : searchFields) { for (String field : searchFields) {
searchList.put(field, search); searchList.put(field, search);
} }
}
return searchList; return searchList;
} }
/** /**
* Constructs a XForm that is returned as an IQ packet that contains the search results. * Constructs a XForm that is returned as an IQ packet that contains the
* search results.
* *
* @param users set of users that will be used to construct the search results * @param users
* @param packet the IQ packet sent by the client * set of users that will be used to construct the search results
* @param packet
* the IQ packet sent by the client
* @return the iq packet that contains the search results * @return the iq packet that contains the search results
*/ */
private IQ replyDataFormResult(Set<User> users, IQ packet) { private IQ replyDataFormResult(Collection<User> users, IQ packet) {
XDataFormImpl searchResults = new XDataFormImpl(DataForm.TYPE_RESULT); XDataFormImpl searchResults = new XDataFormImpl(DataForm.TYPE_RESULT);
XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE"); XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
...@@ -403,22 +617,27 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -403,22 +617,27 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
IQ replyPacket = IQ.createResultIQ(packet); IQ replyPacket = IQ.createResultIQ(packet);
Element reply = replyPacket.setChildElement("query", "jabber:iq:search"); Element reply = replyPacket
.setChildElement("query", NAMESPACE_JABBER_IQ_SEARCH);
reply.add(searchResults.asXMLElement()); reply.add(searchResults.asXMLElement());
return replyPacket; return replyPacket;
} }
/** /**
* Constructs a query that is returned as an IQ packet that contains the search results. * Constructs a query that is returned as an IQ packet that contains the
* search results.
* *
* @param users set of users that will be used to construct the search results * @param users
* @param packet the IQ packet sent by the client * set of users that will be used to construct the search results
* @param packet
* the IQ packet sent by the client
* @return the iq packet that contains the search results * @return the iq packet that contains the search results
*/ */
private IQ replyNonDataFormResult(Set<User> users, IQ packet) { private IQ replyNonDataFormResult(Collection<User> users, IQ packet) {
IQ replyPacket = IQ.createResultIQ(packet); IQ replyPacket = IQ.createResultIQ(packet);
Element replyQuery = replyPacket.setChildElement("query", "jabber:iq:search"); Element replyQuery = replyPacket.setChildElement("query",
NAMESPACE_JABBER_IQ_SEARCH);
for (User user : users) { for (User user : users) {
Element item = replyQuery.addElement("item"); Element item = replyQuery.addElement("item");
...@@ -427,20 +646,23 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -427,20 +646,23 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
item.addAttribute("jid", username + "@" + serverName); item.addAttribute("jid", username + "@" + serverName);
//return to the client the same fields that were submitted // return to the client the same fields that were submitted
for (String field : reverseFieldLookup.keySet()) { for (String field : reverseFieldLookup.keySet()) {
if ("Username".equals(field)) { if ("Username".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field)); Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(username); element.addText(username);
} }
if ("Name".equals(field)) { if ("Name".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field)); Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(removeNull(user.getName())); element.addText(removeNull(user.getName()));
} }
if ("Email".equals(field)) { if ("Email".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field)); Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(removeNull(user.getEmail())); element.addText(removeNull(user.getEmail()));
} }
} }
...@@ -459,11 +681,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -459,11 +681,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* Sets the service name of this component, which is "search" by default. If the name * Sets the service name of this component, which is "search" by
* is different than the existing name the plugin will remove itself from the ComponentManager * default. If the name is different than the existing name the plugin
* and then add itself back using the new name. * will remove itself from the ComponentManager and then add itself back
* using the new name.
* *
* @param name the service name of this component. * @param name
* the service name of this component.
*/ */
public void setServiceName(String name) { public void setServiceName(String name) {
changeServiceName(name); changeServiceName(name);
...@@ -471,6 +695,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -471,6 +695,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* Checks if the search service is currently enabled.
*
* @return true if search service is enabled. * @return true if search service is enabled.
*/ */
public boolean getServiceEnabled() { public boolean getServiceEnabled() {
...@@ -478,11 +704,12 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -478,11 +704,12 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* Enables or disables the search service. When disabled, when a client tries * Enables or disables the search service. When disabled, when a client
* to do a search they will receive an XForm informing that the service is * tries to do a search they will receive an XForm informing that the
* unavailable. * service is unavailable.
* *
* @param enabled true if group permission checking should be disabled. * @param enabled
* true if group permission checking should be disabled.
*/ */
public void setServiceEnabled(boolean enabled) { public void setServiceEnabled(boolean enabled) {
serviceEnabled = enabled; serviceEnabled = enabled;
...@@ -490,8 +717,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -490,8 +717,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* Returns the collection of searchable field names that does not include the fields * Returns the collection of searchable field names that does not include
* listed in the EXCLUDEDFIELDS property list. * the fields listed in the EXCLUDEDFIELDS property list.
*/ */
public Collection<String> getFilteredSearchFields() { public Collection<String> getFilteredSearchFields() {
Collection<String> searchFields; Collection<String> searchFields;
...@@ -500,8 +727,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -500,8 +727,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
// by providing our own searching. // by providing our own searching.
try { try {
searchFields = new ArrayList<String>(userManager.getSearchFields()); searchFields = new ArrayList<String>(userManager.getSearchFields());
} } catch (UnsupportedOperationException uoe) {
catch (UnsupportedOperationException uoe) {
// Use a SearchPluginUserManager instead. // Use a SearchPluginUserManager instead.
searchFields = getSearchPluginUserManagerSearchFields(); searchFields = getSearchPluginUserManagerSearchFields();
} }
...@@ -512,46 +738,53 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -512,46 +738,53 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* Restricts which fields can be searched on and shown to clients. This can be used * Restricts which fields can be searched on and shown to clients. This can
* in the case of preventing users email addresses from being revealed as part of * be used in the case of preventing users email addresses from being
* the search results. * revealed as part of the search results.
* *
* @param set of fields that can not be searched on or shown to the client * @param exculudedFields
* set of fields that can not be searched on or shown to the
* client
*/ */
public void setExcludedFields(Collection<String> exculudedFields) { public void setExcludedFields(Collection<String> exculudedFields) {
this.exculudedFields = exculudedFields; this.exculudedFields = exculudedFields;
JiveGlobals.setProperty(EXCLUDEDFIELDS, StringUtils.collectionToString(exculudedFields)); JiveGlobals.setProperty(EXCLUDEDFIELDS, StringUtils
.collectionToString(exculudedFields));
} }
public void propertySet(String property, Map params) { /*
* (non-Javadoc)
*
* @see org.jivesoftware.util.PropertyEventListener#propertySet(java.lang.String,
* java.util.Map)
*/
public void propertySet(String property, Map<String, Object> params) {
if (property.equals(SERVICEENABLED)) { if (property.equals(SERVICEENABLED)) {
this.serviceEnabled = Boolean.parseBoolean((String)params.get("value")); this.serviceEnabled = Boolean.parseBoolean((String) params
} .get("value"));
else if (property.equals(SERVICENAME)) { } else if (property.equals(SERVICENAME)) {
changeServiceName((String)params.get("value")); changeServiceName((String) params.get("value"));
} } else if (property.equals(EXCLUDEDFIELDS)) {
else if (property.equals(EXCLUDEDFIELDS)) { exculudedFields = StringUtils.stringToCollection(JiveGlobals
exculudedFields = StringUtils.stringToCollection(JiveGlobals.getProperty(EXCLUDEDFIELDS, (String)params.get("value"))); .getProperty(EXCLUDEDFIELDS, (String) params.get("value")));
} }
} }
public void propertyDeleted(String property, Map params) { public void propertyDeleted(String property, Map<String, Object> params) {
if (property.equals(SERVICEENABLED)) { if (property.equals(SERVICEENABLED)) {
this.serviceEnabled = true; this.serviceEnabled = true;
} } else if (property.equals(SERVICENAME)) {
else if (property.equals(SERVICENAME)) {
changeServiceName("search"); changeServiceName("search");
} } else if (property.equals(EXCLUDEDFIELDS)) {
else if (property.equals(EXCLUDEDFIELDS)) {
exculudedFields = new ArrayList<String>(); exculudedFields = new ArrayList<String>();
} }
} }
public void xmlPropertySet(String property, Map params) { public void xmlPropertySet(String property, Map<String, Object> params) {
// not used // not used
} }
public void xmlPropertyDeleted(String property, Map params) { public void xmlPropertyDeleted(String property, Map<String, Object> params) {
// not used // not used
} }
...@@ -567,22 +800,20 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -567,22 +800,20 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
// Re-register the service. // Re-register the service.
try { try {
componentManager.removeComponent(this.serviceName); componentManager.removeComponent(this.serviceName);
} } catch (Exception e) {
catch (Exception e) {
componentManager.getLog().error(e); componentManager.getLog().error(e);
} }
try { try {
componentManager.addComponent(serviceName, this); componentManager.addComponent(serviceName, this);
} } catch (Exception e) {
catch (Exception e) {
componentManager.getLog().error(e); componentManager.getLog().error(e);
} }
this.serviceName = serviceName; this.serviceName = serviceName;
} }
private class CaseInsensitiveComparator implements Comparator<String> { class CaseInsensitiveComparator implements Comparator<String> {
public int compare(String s1, String s2) { public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2); return s1.compareToIgnoreCase(s2);
} }
...@@ -612,8 +843,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -612,8 +843,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
* part of queries. * part of queries.
* *
* A possible future improvement would be to have a third parameter that * A possible future improvement would be to have a third parameter that
* sets the maximum number of users returned and/or the number of users * sets the maximum number of users returned and/or the number of users that
* that are searched. * are searched.
*/ */
public Collection<User> findUsers(String field, String query) { public Collection<User> findUsers(String field, String query) {
List<User> foundUsers = new ArrayList<User>(); List<User> foundUsers = new ArrayList<User>();
...@@ -630,17 +861,14 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -630,17 +861,14 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
try { try {
foundUsers.add(userManager.getUser(query)); foundUsers.add(userManager.getUser(query));
return foundUsers; return foundUsers;
} } catch (UserNotFoundException e) {
catch (UserNotFoundException e) {
Log.error("Error getting user", e); Log.error("Error getting user", e);
} }
} } else if (field.equals("Name")) {
else if (field.equals("Name")) {
if (query.equalsIgnoreCase(user.getName())) { if (query.equalsIgnoreCase(user.getName())) {
foundUsers.add(user); foundUsers.add(user);
} }
} } else if (field.equals("Email")) {
else if (field.equals("Email")) {
if (user.getEmail() != null) { if (user.getEmail() != null) {
if (query.equalsIgnoreCase(user.getEmail())) { if (query.equalsIgnoreCase(user.getEmail())) {
foundUsers.add(user); foundUsers.add(user);
...@@ -648,19 +876,16 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -648,19 +876,16 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
} }
} }
} } else {
else {
String prefix = query.substring(0, index); String prefix = query.substring(0, index);
Collection<User> users = userManager.getUsers(); Collection<User> users = userManager.getUsers();
for (User user : users) { for (User user : users) {
String userInfo = ""; String userInfo = "";
if (field.equals("Username")) { if (field.equals("Username")) {
userInfo = user.getUsername(); userInfo = user.getUsername();
} } else if (field.equals("Name")) {
else if (field.equals("Name")) {
userInfo = user.getName(); userInfo = user.getName();
} } else if (field.equals("Email")) {
else if (field.equals("Email")) {
userInfo = user.getEmail() == null ? "" : user.getEmail(); userInfo = user.getEmail() == null ? "" : user.getEmail();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment