Commit 69bd190b authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

Added sesult set management. JM-1147

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@9294 b35dd754-fafc-0310-a699-88a17e54d16e
parent 8c78affa
...@@ -10,33 +10,35 @@ package org.jivesoftware.openfire.plugin; ...@@ -10,33 +10,35 @@ package org.jivesoftware.openfire.plugin;
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;
import org.jivesoftware.openfire.disco.IQDiscoInfoHandler;
import org.jivesoftware.openfire.disco.IQDiscoItemsHandler;
import org.jivesoftware.openfire.forms.DataForm; import org.jivesoftware.openfire.forms.DataForm;
import org.jivesoftware.openfire.forms.FormField; import org.jivesoftware.openfire.forms.FormField;
import org.jivesoftware.openfire.forms.spi.XDataFormImpl; import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
import org.jivesoftware.openfire.forms.spi.XFormFieldImpl; import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.openfire.resultsetmanager.ResultSet;
import org.jivesoftware.openfire.resultsetmanager.ResultSetImpl;
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.*;
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;
import org.xmpp.component.ComponentManagerFactory; import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.IQ; import org.xmpp.packet.IQ;
import org.xmpp.packet.IQ.Type;
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.PacketError;
import org.xmpp.packet.PacketError.Condition;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.Map.Entry;
/** /**
* Provides support for Jabber Search * Provides support for Jabber Search
...@@ -52,6 +54,7 @@ import java.util.*; ...@@ -52,6 +54,7 @@ import java.util.*;
* @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_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";
...@@ -69,6 +72,21 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -69,6 +72,21 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
private TreeMap<String, String> fieldLookup = new TreeMap<String, String>(new CaseInsensitiveComparator()); private 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>();
/**
* 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() { public SearchPlugin() {
serviceName = JiveGlobals.getProperty(SERVICENAME, "search"); serviceName = JiveGlobals.getProperty(SERVICENAME, "search");
serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true); serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);
...@@ -88,14 +106,30 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -88,14 +106,30 @@ 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;
...@@ -109,12 +143,28 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -109,12 +143,28 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
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) {
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#start()
*/
public void start() { public void start() {
} }
/*
* (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;
...@@ -135,130 +185,231 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -135,130 +185,231 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
reverseFieldLookup = null; reverseFieldLookup = null;
} }
/*
* (non-Javadoc)
*
* @see org.xmpp.component.Component#shutdown()
*/
public void shutdown() { public void shutdown() {
} }
/*
* (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; return;
}
final IQ packet = (IQ) p;
Element childElement = (packet).getChildElement(); if (packet.getType().equals(IQ.Type.error)
String namespace = null; || packet.getType().equals(IQ.Type.result)) {
if (childElement != null) { return;
namespace = childElement.getNamespaceURI(); }
}
if ("jabber:iq:search".equals(namespace)) { // Packet p is an IQ stanza of type GET or SET. Therefor, it _must_ be
try { // replied to.
IQ replyPacket = handleIQ(packet); final IQ replyPacket = handleIQRequest(packet);
if (replyPacket != null) {
componentManager.sendPacket(this, replyPacket); try {
} 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#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);
}
catch (ComponentException 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);
}
}
} }
} }
private IQ handleIQ(IQ packet) { /**
if (!serviceEnabled) { * Handles IQ requests. This method throws an IllegalArgumentException if an
return replyDisabled(packet); * 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.
if (iq == null) {
throw new IllegalArgumentException("Argument 'iq' cannot be null.");
} }
if (IQ.Type.get.equals(packet.getType())) { final IQ.Type type = iq.getType();
return processGetPacket(packet); if (type != IQ.Type.get && type != IQ.Type.set) {
throw new IllegalArgumentException(
"Argument 'iq' must be of type 'get' or 'set'");
} }
else if (IQ.Type.set.equals(packet.getType())) {
return processSetPacket(packet); 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 (IQ.Type.result.equals(packet.getType()) || IQ.Type.error.equals(packet.getType())) {
// Ignore final String namespace = childElement.getNamespaceURI();
if (namespace == null) {
replyPacket = IQ.createResultIQ(iq);
replyPacket.setError(Condition.feature_not_implemented);
return replyPacket;
} }
else {
// Unknown type was sent so return an error if (namespace.equals(NAMESPACE_JABBER_IQ_SEARCH)) {
IQ reply = new IQ(IQ.Type.error, packet.getID()); replyPacket = handleSearchRequest(iq);
reply.setFrom(packet.getTo()); } else if (namespace.equals(IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)) {
reply.setTo(packet.getFrom()); replyPacket = handleDiscoInfo(iq);
reply.setError(PacketError.Condition.bad_request); } else if (namespace.equals(IQDiscoItemsHandler.NAMESPACE_DISCO_ITEMS)) {
return reply; 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 null; return replyPacket;
} }
private IQ replyDisabled(IQ packet) { /**
* 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.");
}
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);
}
switch (packet.getType()) {
case get:
return processGetPacket(packet);
case set:
return processSetPacket(packet);
default:
// we can safely ignore 'error' and 'result' typed iq stanzas.
return null;
}
}
/**
* 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()) {
// non-data form
queryResult.addElement(searchField);
for (String searchField : getFilteredSearchFields()) {
field = new XFormFieldImpl(searchField); field = new XFormFieldImpl(searchField);
field.setType(FormField.TYPE_BOOLEAN); field.setType(FormField.TYPE_BOOLEAN);
field.addValue("1"); 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); field.setRequired(false);
searchForm.addField(field); searchForm.addField(field);
} }
...@@ -269,114 +420,282 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -269,114 +420,282 @@ 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) {
if (!packet.getType().equals(IQ.Type.set)) {
throw new IllegalArgumentException(
"This method only accepts 'set' typed IQ stanzas as an argument.");
}
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>(); Set<User> users = new HashSet<User>();
Element incomingForm = packet.getChildElement();
boolean isDataFormQuery = (incomingForm.element(QName.get("x", "jabber:x:data")) != null);
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
foundUsers.addAll(userManager.findUsers(new HashSet<String>( && !query.equals(NAMESPACE_JABBER_IQ_SEARCH)) {
Arrays.asList((field))), query)); foundUsers
.addAll(userManager.findUsers(new HashSet<String>(
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>();
while (iter.hasNext()) {
Element element = (Element) iter.next(); // since not all clients request which fields are available for
String name = element.getName(); // searching attempt to match submitted fields with available search
// fields
if (fieldLookup.containsKey(name)) { Iterator<Element> iter = incomingForm.elementIterator();
//make best effort to map the fields submitted by while (iter.hasNext()) {
//the client to those that Openfire can search Element element = iter.next();
reverseFieldLookup.put(fieldLookup.get(name), name); String name = element.getName();
searchList.put(fieldLookup.get(name), element.getText());
} if (fieldLookup.containsKey(name)) {
// 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());
} }
} }
else {
List<String> searchFields = new ArrayList<String>(); return searchList;
String search = ""; }
Iterator fields = form.elementIterator("field"); /**
while (fields.hasNext()) { * Extracts a search query from a data form that makes use of data forms to
Element searchField = (Element) fields.next(); * specify the search request. This 'extended' way of constructing a search
* request is documented in XEP-0055, chapter 3.
String field = searchField.attributeValue("var"); *
String value = ""; * @param incomingForm
if (searchField.element("value") != null) { * The form from which to extract the query
value = searchField.element("value").getTextTrim(); * @return The search query for a particular user search request.
} * @see #extractSearchQuery(Element)
if (field.equals("search")) { */
search = value; private Hashtable<String, String> extractExtendedSearchQuery(
} Element incomingForm) {
else if (value.equals("1")) { final Element dataform = incomingForm.element(QName.get("x",
searchFields.add(field); "jabber:x:data"));
}
Hashtable<String, String> searchList = new Hashtable<String, String>();
List<String> searchFields = new ArrayList<String>();
String search = "";
Iterator<Element> fields = dataform.elementIterator("field");
while (fields.hasNext()) {
Element searchField = fields.next();
String field = searchField.attributeValue("var");
String value = "";
if (searchField.element("value") != null) {
value = searchField.element("value").getTextTrim();
} }
if (field.equals("search")) {
for (String field : searchFields) { search = value;
searchList.put(field, search); } else if (value.equals("1")) {
searchFields.add(field);
} }
} }
for (String field : searchFields) {
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 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 set of users that will be used to construct the search results
* @param packet the IQ packet sent by the client * @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");
field.setType(FormField.TYPE_HIDDEN); field.setType(FormField.TYPE_HIDDEN);
searchResults.addField(field); searchResults.addField(field);
field = new XFormFieldImpl("jid"); field = new XFormFieldImpl("jid");
field.setLabel("JID"); field.setLabel("JID");
searchResults.addReportedField(field); searchResults.addReportedField(field);
for (String fieldName : getFilteredSearchFields()) { for (String fieldName : getFilteredSearchFields()) {
field = new XFormFieldImpl(fieldName); 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); searchResults.addReportedField(field);
} }
...@@ -384,7 +703,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -384,7 +703,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
String username = JID.unescapeNode(user.getUsername()); String username = JID.unescapeNode(user.getUsername());
ArrayList<XFormFieldImpl> items = new ArrayList<XFormFieldImpl>(); ArrayList<XFormFieldImpl> items = new ArrayList<XFormFieldImpl>();
XFormFieldImpl fieldJID = new XFormFieldImpl("jid"); XFormFieldImpl fieldJID = new XFormFieldImpl("jid");
fieldJID.addValue(username + "@" + serverName); fieldJID.addValue(username + "@" + serverName);
items.add(fieldJID); items.add(fieldJID);
...@@ -405,7 +724,8 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -405,7 +724,8 @@ 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;
...@@ -413,36 +733,38 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -413,36 +733,38 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
/** /**
* 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 set of users that will be used to construct the search results
* @param packet the IQ packet sent by the client * @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");
String username = JID.unescapeNode(user.getUsername()); String username = JID.unescapeNode(user.getUsername());
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()));
} }
} }
...@@ -450,7 +772,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -450,7 +772,7 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
return replyPacket; return replyPacket;
} }
/** /**
* Returns the service name of this component, which is "search" by default. * Returns the service name of this component, which is "search" by default.
* *
...@@ -473,7 +795,9 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -473,7 +795,9 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
/** /**
* @return true if search service is enabled. * Checks if the search service is enabled.
*
* @return true if search service is enabled.
*/ */
public boolean getServiceEnabled() { public boolean getServiceEnabled() {
return serviceEnabled; return serviceEnabled;
...@@ -527,7 +851,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -527,7 +851,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
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"));
} }
...@@ -539,7 +869,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -539,7 +869,13 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
} }
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)) { if (property.equals(SERVICEENABLED)) {
this.serviceEnabled = true; this.serviceEnabled = true;
} }
...@@ -551,11 +887,23 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -551,11 +887,23 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
} }
} }
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 // 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 // not used
} }
...@@ -586,12 +934,24 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener { ...@@ -586,12 +934,24 @@ public class SearchPlugin implements Component, Plugin, PropertyEventListener {
this.serviceName = serviceName; this.serviceName = serviceName;
} }
/**
* Comparator that compares String objects, ignoring capitalization.
*/
private class CaseInsensitiveComparator implements Comparator<String> { private 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);
} }
} }
/**
* 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) { private String removeNull(String s) {
if (s == null) { if (s == null) {
return ""; return "";
......
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