Commit 87a9a68d authored by guus's avatar guus

Result Set Management applied to the user search plugin. This effectively...

Result Set Management applied to the user search plugin. This effectively enables users to page through a potentially large number of results.

User search wasn't compliant with XEP-0055 in the basic search (invalid search fields were used). I replaced them.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/branches/rsm@9169 b35dd754-fafc-0310-a699-88a17e54d16e
parent bbefea67
......@@ -44,6 +44,12 @@
Search Plugin Changelog
</h1>
<p><b>1.5.0</b> -- September 19, 2007 (GdK)</p>
<ul>
<li>Now implements XEP-0059 "Result Set Management".</li>
<li>Basic search (not 'extended search') is now XEP-0055 compliant.</li>
</ul>
<p><b>1.4.1</b> -- June 20, 2007</p>
<ul>
<li>Unescape username before returning search results to the client.</li>
......
......@@ -5,8 +5,8 @@
<name>Search</name>
<description>Provides support for Jabber Search (XEP-0055)</description>
<author>Ryan Graham</author>
<version>1.4.1</version>
<date>6/20/2007</date>
<version>1.5.0</version>
<date>9/19/2007</date>
<minServerVersion>3.3.0</minServerVersion>
<adminconsole>
......
......@@ -7,18 +7,29 @@
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.Element;
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.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
import org.jivesoftware.openfire.disco.IQDiscoItemsHandler;
import org.jivesoftware.openfire.forms.DataForm;
import org.jivesoftware.openfire.forms.FormField;
import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
......@@ -26,6 +37,14 @@ import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
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.resultsetmanager.ResultSet;
import org.jivesoftware.util.resultsetmanager.ResultSetImpl;
import org.xmpp.component.Component;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
......@@ -34,51 +53,78 @@ import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import java.io.File;
import java.util.*;
import org.xmpp.packet.IQ.Type;
import org.xmpp.packet.PacketError.Condition;
/**
* Provides support for Jabber Search
* (<a href="http://www.xmpp.org/extensions/xep-0055.html">XEP-0055</a>).<p>
* Provides support for Jabber Search (<a
* href="http://www.xmpp.org/extensions/xep-0055.html">XEP-0055</a>).
* <p>
*
* The basic functionality is to query an information repository
* regarding the possible search fields, to send a search query,
* and to receive search results. This implementation was primarily designed to use
* <a href="http://www.xmpp.org/extensions/xep-0004.html">Data Forms</a>, but
* also supports non-dataform searches.
* <p/>
* The basic functionality is to query an information repository regarding the
* possible search fields, to send a search query, and to receive search
* results. This implementation was primarily designed to use <a
* href="http://www.xmpp.org/extensions/xep-0004.html">Data Forms</a>, but also
* supports non-dataform searches. <p/>
*
* @author <a href="mailto:ryan@version2software.com">Ryan Graham</a>
*/
public class SearchPlugin implements Component, Plugin, PropertyEventListener {
public static final String NAMESPACE_JABBER_IQ_SEARCH = "jabber:iq:search";
public static final String SERVICENAME = "plugin.search.serviceName";
public static final String SERVICEENABLED = "plugin.search.serviceEnabled";
public static final String EXCLUDEDFIELDS = "plugin.search.excludedFields";
private UserManager userManager;
private final UserManager userManager;
private ComponentManager componentManager;
private PluginManager pluginManager;
private String serviceName;
private boolean serviceEnabled;
private Collection<String> exculudedFields;
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>();
/**
* A list of field names that are valid in jabber:iq:search
*/
public final static Collection<String> validSearchRequestFields = new ArrayList<String>();
static {
validSearchRequestFields.add("first");
validSearchRequestFields.add("last");
validSearchRequestFields.add("nick");
validSearchRequestFields.add("email");
validSearchRequestFields.add("x"); // extended info
// result set management (XEP-0059)
validSearchRequestFields.add("set");
}
public SearchPlugin() {
serviceName = JiveGlobals.getProperty(SERVICENAME, "search");
serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);
exculudedFields = StringUtils.stringToCollection(JiveGlobals.getProperty(EXCLUDEDFIELDS, ""));
exculudedFields = StringUtils.stringToCollection(JiveGlobals
.getProperty(EXCLUDEDFIELDS, ""));
serverName = XMPPServer.getInstance().getServerInfo().getName();
userManager = UserManager.getInstance();
// Some clients, such as Miranda, are hard-coded to search specific fields,
// so we map those fields to the fields that Openfire actually supports.
// Some clients, such as Miranda, are hard-coded to search specific
// fields so we map those fields to the fields that Openfire actually
// supports.
fieldLookup.put("jid", "Username");
fieldLookup.put("username", "Username");
fieldLookup.put("first", "Name");
......@@ -88,175 +134,304 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
fieldLookup.put("email", "Email");
}
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#getName()
*/
public String getName() {
return pluginManager.getName(this);
}
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#getDescription()
*/
public String getDescription() {
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) {
pluginManager = manager;
componentManager = ComponentManagerFactory.getComponentManager();
try {
componentManager.addComponent(serviceName, this);
}
catch (ComponentException e) {
} catch (ComponentException e) {
componentManager.getLog().error(e);
}
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) {
// intentionally left blank. See initializePlugin()
}
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#start()
*/
public void start() {
// intentionally left blank. See initializePlugin()
}
/*
* (non-Javadoc)
*
* @see org.jivesoftware.openfire.container.Plugin#destroyPlugin()
*/
public void destroyPlugin() {
PropertyEventDispatcher.removeListener(this);
pluginManager = null;
try {
componentManager.removeComponent(serviceName);
componentManager = null;
}
catch (Exception e) {
} catch (Exception 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() {
// intentionally left blank. See destroyPlugin()
}
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#processPacket(org.xmpp.packet.Packet)
*/
public void processPacket(Packet p) {
if (p instanceof IQ) {
IQ packet = (IQ) p;
if (!(p instanceof IQ)) {
return;
}
final IQ packet = (IQ) p;
Element childElement = (packet).getChildElement();
String namespace = null;
if (childElement != null) {
namespace = childElement.getNamespaceURI();
if (packet.getType().equals(IQ.Type.error)
|| packet.getType().equals(IQ.Type.result)) {
return;
}
if ("jabber:iq:search".equals(namespace)) {
// Packet p is an IQ stanza of type GET or SET. Therefor, it _must_ be
// replied to.
final IQ replyPacket = handleIQRequest(packet);
try {
IQ replyPacket = handleIQ(packet);
if (replyPacket != null) {
componentManager.sendPacket(this, replyPacket);
}
}
catch (ComponentException e) {
} catch (ComponentException e) {
componentManager.getLog().error(e);
}
}
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");
/**
* Handles IQ requests. This method throws an IllegalArgumentException if an
* IQ stanza is supplied that is not a request (if the stanza is not of type
* 'get' or 'set'). This method will either throw an Exception, or return a
* non-null IQ stanza of type 'error' or 'result', as XMPP Core specifies
* that <strong>all</strong> IQ request stanza's (type 'get' or 'set') MUST
* be replied to.
*
* @param iq
* The IQ stanza that forms the request.
* @return The response to the request.
*/
private IQ handleIQRequest(IQ iq) {
final IQ replyPacket; // 'final' to ensure that it is set.
componentManager.sendPacket(this, replyPacket);
if (iq == null) {
throw new IllegalArgumentException("Argument 'iq' cannot be null.");
}
catch (ComponentException e) {
componentManager.getLog().error(e);
final IQ.Type type = iq.getType();
if (type != IQ.Type.get && type != IQ.Type.set) {
throw new IllegalArgumentException(
"Argument 'iq' must be of type 'get' or 'set'");
}
final Element childElement = iq.getChildElement();
if (childElement == null) {
replyPacket = IQ.createResultIQ(iq);
replyPacket
.setError(new PacketError(
Condition.bad_request,
org.xmpp.packet.PacketError.Type.modify,
"IQ stanzas of type 'get' and 'set' MUST contain one and only one child element (RFC 3920 section 9.2.3)."));
return replyPacket;
}
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);
final String namespace = childElement.getNamespaceURI();
if (namespace == null) {
replyPacket = IQ.createResultIQ(iq);
replyPacket.setError(Condition.feature_not_implemented);
return replyPacket;
}
catch (ComponentException e) {
componentManager.getLog().error(e);
if (namespace.equals(NAMESPACE_JABBER_IQ_SEARCH)) {
replyPacket = handleSearchRequest(iq);
} else if (namespace.equals(IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)) {
replyPacket = handleDiscoInfo(iq);
} else if (namespace.equals(IQDiscoItemsHandler.NAMESPACE_DISCO_ITEMS)) {
replyPacket = IQ.createResultIQ(iq);
replyPacket.setChildElement("query",
IQDiscoItemsHandler.NAMESPACE_DISCO_ITEMS);
} else {
// don't known what to do with this.
replyPacket = IQ.createResultIQ(iq);
replyPacket.setError(Condition.feature_not_implemented);
}
return replyPacket;
}
/**
* 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(
IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)
|| iq.getType() != Type.get) {
throw new IllegalArgumentException(
"This is not a valid disco#info request.");
}
private IQ handleIQ(IQ packet) {
final IQ replyPacket = IQ.createResultIQ(iq);
final Element responseElement = replyPacket.setChildElement("query",
IQDiscoInfoHandler.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",
IQDiscoInfoHandler.NAMESPACE_DISCO_INFO);
responseElement.addElement("feature").addAttribute("var",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);
return replyPacket;
}
private IQ handleSearchRequest(IQ packet) {
if (!serviceEnabled) {
return replyDisabled(packet);
}
if (IQ.Type.get.equals(packet.getType())) {
switch (packet.getType()) {
case get:
return processGetPacket(packet);
}
else if (IQ.Type.set.equals(packet.getType())) {
case set:
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;
}
}
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);
Element reply = replyPacket.setChildElement("query", "jabber:iq:search");
Element reply = replyPacket.setChildElement("query",
NAMESPACE_JABBER_IQ_SEARCH);
XDataFormImpl unavailableForm = new XDataFormImpl(DataForm.TYPE_CANCEL);
unavailableForm.setTitle(LocaleUtils.getLocalizedString("advance.user.search.title", "search"));
unavailableForm.addInstruction(LocaleUtils.getLocalizedString("search.service_unavailable", "search"));
unavailableForm.setTitle(LocaleUtils.getLocalizedString(
"advance.user.search.title", "search"));
unavailableForm.addInstruction(LocaleUtils.getLocalizedString(
"search.service_unavailable", "search"));
reply.add(unavailableForm.asXMLElement());
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) {
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);
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
queryResult.addElement("instructions").addText(instructions);
queryResult.addElement("first");
queryResult.addElement("last");
queryResult.addElement("nick");
queryResult.addElement("email");
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);
XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
field.setType(FormField.TYPE_HIDDEN);
field.addValue("jabber:iq:search");
field.addValue(NAMESPACE_JABBER_IQ_SEARCH);
searchForm.addField(field);
field = new XFormFieldImpl("search");
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);
searchForm.addField(field);
for (String searchField : getFilteredSearchFields()) {
// non-data form
queryResult.addElement(searchField);
field = new XFormFieldImpl(searchField);
field.setType(FormField.TYPE_BOOLEAN);
field.addValue("1");
field.setLabel(LocaleUtils.getLocalizedString("advance.user.search." + searchField.toLowerCase(), "search"));
field.setLabel(LocaleUtils.getLocalizedString(
"advance.user.search." + searchField.toLowerCase(), "search"));
field.setRequired(false);
searchForm.addField(field);
}
......@@ -267,71 +442,240 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
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) {
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();
boolean isDataFormQuery = (incomingForm.element(QName.get("x", "jabber:x:data")) != null);
final IQ resultIQ;
// check if the request complies to the XEP-0055 standards
if (!isValidSearchRequest(packet)) {
resultIQ = IQ.createResultIQ(packet);
resultIQ.setError(Condition.bad_request);
return resultIQ;
}
final Element incomingForm = packet.getChildElement();
final boolean isDataFormQuery = (incomingForm.element(QName.get("x",
"jabber:x:data")) != null);
final Set<User> searchResults = performSearch(incomingForm);
final Element rsmElement = incomingForm.element(QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
final boolean applyRSM = rsmElement != null && !searchResults.isEmpty();
if (applyRSM) {
// apply RSM
final List<User> rsmResults;
final ResultSet<User> rs = new ResultSetImpl<User>(searchResults);
try {
rsmResults = rs.applyRSMDirectives(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 set = rs.generateSetElementFromResults(rsmResults);
resultIQ.getChildElement().add(set);
} else {
// don't apply RSM
if (isDataFormQuery) {
resultIQ = replyDataFormResult(searchResults, packet);
} else {
resultIQ = replyNonDataFormResult(searchResults, packet);
}
}
return resultIQ;
}
/**
* This method checks if the search request that was received is a valid
* JABBER:IQ:SEARCH request. In other words, it checks if the search request
* is spec compliant (XEP-0055). It does this by checking:
* <ul>
* <li>if the IQ stanza is of type 'set';</li>
* <li>if a child element identified by the jabber:iq:search namespace is
* supplied;</li>
* <li>if the stanza child element is has valid children itself.</li>
* </ul>
*
* @param iq
* The IQ object that should include a jabber:iq:search request.
* @return ''true'' if the supplied IQ stanza is a spec compliant search
* request, ''false'' otherwise.
*/
public static boolean isValidSearchRequest(IQ iq) {
if (iq == null) {
throw new IllegalArgumentException("Argument 'iq' cannot be null.");
}
if (iq.getType() != IQ.Type.set) {
return false;
}
final Element childElement = iq.getChildElement();
if (childElement == null) {
return false;
}
if (!childElement.getNamespaceURI().equals(NAMESPACE_JABBER_IQ_SEARCH)) {
return false;
}
if (!childElement.getName().equals("query")) {
return false;
}
final List<Element> fields = childElement.elements();
if (fields.size() == 0) {
return false;
}
for (Element element : fields) {
final String name = element.getName();
if (!validSearchRequestFields.contains(name)) {
return false;
}
// TODO: check dataform validity.
// if (name.equals("x") && !isValidDataForm(element))
// {
// return false;
// }
if (name.equals("set") && !ResultSet.isValidRSMRequest(element)) {
return false;
}
}
return true;
}
/**
* 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);
Enumeration<String> searchIter = searchList.keys();
while (searchIter.hasMoreElements()) {
String field = searchIter.nextElement();
String query = searchList.get(field);
for (Entry<String, String> entry : searchList.entrySet()) {
String field = entry.getKey();
String query = entry.getValue();
Collection<User> foundUsers = new ArrayList<User>();
if (userManager != null) {
if (query.length() > 0 && !query.equals("jabber:iq:search")) {
foundUsers.addAll(userManager.findUsers(new HashSet<String>(
if (query.length() > 0
&& !query.equals(NAMESPACE_JABBER_IQ_SEARCH)) {
foundUsers
.addAll(userManager.findUsers(new HashSet<String>(
Arrays.asList((field))), query));
}
}
else {
} else {
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) {
if (user != null) {
users.add(user);
}
}
}
if (isDataFormQuery) {
return replyDataFormResult(users, packet);
}
else {
return replyNonDataFormResult(users, packet);
}
return users;
}
/**
* 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) {
Hashtable<String, String> searchList = new Hashtable<String, String>();
Element form = incomingForm.element(QName.get("x", "jabber:x:data"));
if (form == null) {
//since not all clients request which fields are available for searching
//attempt to match submitted fields with available search fields
Iterator iter = incomingForm.elementIterator();
if (incomingForm.element(QName.get("x", "jabber:x:data")) != null) {
// forward the request.
return extractExtendedSearchQuery(incomingForm);
}
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()) {
Element element = (Element) iter.next();
Element element = iter.next();
String name = element.getName();
if (fieldLookup.containsKey(name)) {
//make best effort to map the fields submitted by
//the client to those that Openfire can search
// make best effort to map the fields submitted by
// the client to those that Openfire can search
reverseFieldLookup.put(fieldLookup.get(name), name);
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>();
String search = "";
Iterator fields = form.elementIterator("field");
Iterator<Element> fields = dataform.elementIterator("field");
while (fields.hasNext()) {
Element searchField = (Element) fields.next();
Element searchField = fields.next();
String field = searchField.attributeValue("var");
String value = "";
......@@ -340,8 +684,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
if (field.equals("search")) {
search = value;
}
else if (value.equals("1")) {
} else if (value.equals("1")) {
searchFields.add(field);
}
}
......@@ -349,19 +692,18 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
for (String field : searchFields) {
searchList.put(field, search);
}
}
return searchList;
}
/**
* Constructs a XForm 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 packet the IQ packet sent by the client
* @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);
XFormFieldImpl field = new XFormFieldImpl("FORM_TYPE");
......@@ -374,7 +716,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
for (String fieldName : getFilteredSearchFields()) {
field = new XFormFieldImpl(fieldName);
field.setLabel(LocaleUtils.getLocalizedString("advance.user.search." + fieldName.toLowerCase(), "search"));
field.setLabel(LocaleUtils.getLocalizedString(
"advance.user.search." + fieldName.toLowerCase(), "search"));
searchResults.addReportedField(field);
}
......@@ -403,7 +746,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
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());
return replyPacket;
......@@ -416,31 +760,33 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
* @param packet the IQ packet sent by the client
* @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);
Element replyQuery = replyPacket.setChildElement("query", "jabber:iq:search");
Element replyQuery = replyPacket.setChildElement("query",
NAMESPACE_JABBER_IQ_SEARCH);
for (User user : users) {
Element item = replyQuery.addElement("item");
String username = JID.unescapeNode(user.getUsername());
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()) {
if ("Username".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field));
Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(username);
}
if ("Name".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field));
Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(removeNull(user.getName()));
}
if ("Email".equals(field)) {
Element element = item.addElement(reverseFieldLookup.get(field));
Element element = item.addElement(reverseFieldLookup
.get(field));
element.addText(removeNull(user.getEmail()));
}
}
......@@ -471,6 +817,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
/**
* Checks if the search service is enabled.
*
* @return true if search service is enabled.
*/
public boolean getServiceEnabled() {
......@@ -478,11 +826,12 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
/**
* Enables or disables the search service. When disabled, when a client tries
* to do a search they will receive an XForm informing that the service is
* unavailable.
* Enables or disables the search service. When disabled, when a client
* tries to do a search they will receive an XForm informing that the
* 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) {
serviceEnabled = enabled;
......@@ -490,8 +839,10 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
/**
* Returns the collection of searchable field names that does not include the fields
* listed in the EXCLUDEDFIELDS property list.
* Returns the collection of searchable field names that does not include
* the fields listed in the EXCLUDEDFIELDS property list.
*
* @return A collection of field names that can be used in a search request.
*/
public Collection<String> getFilteredSearchFields() {
Collection<String> searchFields;
......@@ -500,8 +851,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
// by providing our own searching.
try {
searchFields = new ArrayList<String>(userManager.getSearchFields());
}
catch (UnsupportedOperationException uoe) {
} catch (UnsupportedOperationException uoe) {
// Use a SearchPluginUserManager instead.
searchFields = getSearchPluginUserManagerSearchFields();
}
......@@ -520,41 +870,71 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
*/
public void setExcludedFields(Collection<String> 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)) {
this.serviceEnabled = Boolean.parseBoolean((String)params.get("value"));
}
else if (property.equals(SERVICENAME)) {
changeServiceName((String)params.get("value"));
}
else if (property.equals(EXCLUDEDFIELDS)) {
exculudedFields = StringUtils.stringToCollection(JiveGlobals.getProperty(EXCLUDEDFIELDS, (String)params.get("value")));
this.serviceEnabled = Boolean.parseBoolean((String) params
.get("value"));
} else if (property.equals(SERVICENAME)) {
changeServiceName((String) params.get("value"));
} else if (property.equals(EXCLUDEDFIELDS)) {
exculudedFields = StringUtils.stringToCollection(JiveGlobals
.getProperty(EXCLUDEDFIELDS, (String) params.get("value")));
}
}
public void propertyDeleted(String property, Map params) {
/*
* (non-Javadoc)
*
* @see org.jivesoftware.util.PropertyEventListener#propertyDeleted(java.lang.String,
* java.util.Map)
*/
public void propertyDeleted(String property, Map<String, Object> params) {
if (property.equals(SERVICEENABLED)) {
this.serviceEnabled = true;
}
else if (property.equals(SERVICENAME)) {
} else if (property.equals(SERVICENAME)) {
changeServiceName("search");
}
else if (property.equals(EXCLUDEDFIELDS)) {
} else if (property.equals(EXCLUDEDFIELDS)) {
exculudedFields = new ArrayList<String>();
}
}
public void xmlPropertySet(String property, Map params) {
/*
* (non-Javadoc)
*
* @see org.jivesoftware.util.PropertyEventListener#xmlPropertySet(java.lang.String,
* java.util.Map)
*/
public void xmlPropertySet(String property, Map<String, Object> params) {
// not used
}
public void xmlPropertyDeleted(String property, Map params) {
/*
* (non-Javadoc)
*
* @see org.jivesoftware.util.PropertyEventListener#xmlPropertyDeleted(java.lang.String,
* java.util.Map)
*/
public void xmlPropertyDeleted(String property, Map<String, Object> params) {
// not used
}
/**
* Changes the name of this service. Note that this method will re-register
* this component if the new name differs from the old name.
*
* @param serviceName
* The new service name.
*/
private void changeServiceName(String serviceName) {
if (serviceName == null) {
throw new NullPointerException("Service name cannot be null");
......@@ -567,27 +947,37 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
// Re-register the service.
try {
componentManager.removeComponent(this.serviceName);
}
catch (Exception e) {
} catch (Exception e) {
componentManager.getLog().error(e);
}
try {
componentManager.addComponent(serviceName, this);
}
catch (Exception e) {
} catch (Exception e) {
componentManager.getLog().error(e);
}
this.serviceName = serviceName;
}
private class CaseInsensitiveComparator implements Comparator<String> {
/**
* Comparator that compares String objects, ignoring capitalization.
*/
class CaseInsensitiveComparator implements Comparator<String> {
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
}
/**
* Returns the trimmed argument, or an empty String object of null was
* supplied as an argument.
*
* @param s
* The String to be trimmed.
* @return String object that does not start or end with whitespace
* characters.
*/
private String removeNull(String s) {
if (s == null) {
return "";
......@@ -612,8 +1002,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
* part of queries.
*
* 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
* that are searched.
* sets the maximum number of users returned and/or the number of users that
* are searched.
*/
public Collection<User> findUsers(String field, String query) {
List<User> foundUsers = new ArrayList<User>();
......@@ -630,17 +1020,14 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
try {
foundUsers.add(userManager.getUser(query));
return foundUsers;
}
catch (UserNotFoundException e) {
} catch (UserNotFoundException e) {
Log.error("Error getting user", e);
}
}
else if (field.equals("Name")) {
} else if (field.equals("Name")) {
if (query.equalsIgnoreCase(user.getName())) {
foundUsers.add(user);
}
}
else if (field.equals("Email")) {
} else if (field.equals("Email")) {
if (user.getEmail() != null) {
if (query.equalsIgnoreCase(user.getEmail())) {
foundUsers.add(user);
......@@ -648,19 +1035,16 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
}
}
}
}
else {
} else {
String prefix = query.substring(0, index);
Collection<User> users = userManager.getUsers();
for (User user : users) {
String userInfo = "";
if (field.equals("Username")) {
userInfo = user.getUsername();
}
else if (field.equals("Name")) {
} else if (field.equals("Name")) {
userInfo = user.getName();
}
else if (field.equals("Email")) {
} else if (field.equals("Email")) {
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