Commit 365f32e1 authored by guus's avatar guus

Added a directory listing service for MUC rooms (jabber:iq:search). Applied...

Added a directory listing service for MUC rooms (jabber:iq:search). Applied the new search to the default MultiUserChatServer implementation, which can act as an example to any other implementation. Added service discovery identity and features to the service that reflect these changes.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/branches/rsm@9177 b35dd754-fafc-0310-a699-88a17e54d16e
parent 035ae2f5
/**
*
*/
package org.jivesoftware.openfire.muc.spi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.forms.DataForm;
import org.jivesoftware.openfire.forms.FormField;
import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.muc.MultiUserChatServer;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.resultsetmanager.ResultSet;
import org.jivesoftware.util.resultsetmanager.ResultSetImpl;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.PacketError.Condition;
/**
* This class adds jabber:iq:search combined with 'result set management'
* functionality to the MUC service of Openfire.
*
* @author Guus der Kinderen - Nimbuzz B.V. <guus@nimbuzz.com>
* @author Giancarlo Frison - Nimbuzz B.V. <giancarlo@nimbuzz.com>
*/
public class IQMUCSearchHandler
{
/**
* The MUC-server to extend with jabber:iq:search functionality.
*/
private final MultiUserChatServer mucServer;
/**
* Creates a new instance of the search provider.
*
* @param mucServer
* The server for which to return search results.
*/
public IQMUCSearchHandler(MultiUserChatServer mucServer)
{
this.mucServer = mucServer;
}
/**
* Utility method that returns a 'jabber:iq:search' child element filled
* with a blank dataform.
*
* @return Element, named 'query', escaped by the 'jabber:iq:search'
* namespace, filled with a blank dataform.
*/
private static Element getDataElement()
{
final XDataFormImpl searchForm = new XDataFormImpl(DataForm.TYPE_FORM);
searchForm.setTitle("Chat Rooms Search");
searchForm.addInstruction("instrunctions");
final FormField typeFF = new XFormFieldImpl("FORM_TYPE");
typeFF.setType(FormField.TYPE_HIDDEN);
typeFF.addValue("jabber:iq:search");
searchForm.addField(typeFF);
final FormField nameFF = new XFormFieldImpl("name");
nameFF.setType(FormField.TYPE_TEXT_SINGLE);
nameFF.setLabel("Name");
nameFF.setRequired(false);
searchForm.addField(nameFF);
final FormField matchFF = new XFormFieldImpl("name_is_exact_match");
matchFF.setType(FormField.TYPE_BOOLEAN);
matchFF.setLabel("Name must match exactly");
matchFF.setRequired(false);
searchForm.addField(matchFF);
final FormField subjectFF = new XFormFieldImpl("subject");
subjectFF.setType(FormField.TYPE_TEXT_SINGLE);
subjectFF.setLabel("Subject");
subjectFF.setRequired(false);
searchForm.addField(subjectFF);
final FormField userAmountFF = new XFormFieldImpl("num_users");
userAmountFF.setType(FormField.TYPE_TEXT_SINGLE);
userAmountFF.setLabel("Number of users");
userAmountFF.setRequired(false);
searchForm.addField(userAmountFF);
final FormField maxUsersFF = new XFormFieldImpl("num_max_users");
maxUsersFF.setType(FormField.TYPE_TEXT_SINGLE);
maxUsersFF.setLabel("Max number allowed of users");
maxUsersFF.setRequired(false);
searchForm.addField(maxUsersFF);
final FormField includePasswordProtectedFF = new XFormFieldImpl(
"include_password_protected");
includePasswordProtectedFF.setType(FormField.TYPE_BOOLEAN);
includePasswordProtectedFF.setLabel("Include password protected rooms");
includePasswordProtectedFF.setRequired(false);
searchForm.addField(includePasswordProtectedFF);
final Element probeResult = DocumentHelper.createElement(QName.get(
"query", "jabber:iq:search"));
probeResult.add(searchForm.asXMLElement());
return probeResult;
}
/**
* Constructs an answer on a IQ stanza that contains a search request. The
* answer will be an IQ stanza of type 'result' or 'error'.
*
* @param iq
* The IQ stanza that is the search request.
* @return An answer to the provided request.
*/
public IQ handleIQ(IQ iq)
{
final IQ reply = IQ.createResultIQ(iq);
final Element formElement = iq.getChildElement().element(
QName.get("x", "jabber:x:data"));
if (formElement == null)
{
reply.setChildElement(getDataElement());
return reply;
}
// parse params from request.
final XDataFormImpl df = new XDataFormImpl();
df.parse(formElement);
boolean name_is_exact_match = false;
String subject = null;
int numusers = -1;
int numaxusers = -1;
boolean includePasswordProtectedRooms = true;
final Set<String> names = new HashSet<String>();
final Iterator<FormField> formFields = df.getFields();
while (formFields.hasNext())
{
final FormField field = formFields.next();
if (field.getVariable().equals("name"))
{
names.add(getFirstValue(field));
}
}
final FormField matchFF = df.getField("name_is_exact_match");
if (matchFF != null)
{
final String b = getFirstValue(matchFF);
if (b != null)
{
name_is_exact_match = b.equals("1")
|| b.equalsIgnoreCase("true")
|| b.equalsIgnoreCase("yes");
}
}
final FormField subjectFF = df.getField("subject");
if (subjectFF != null)
{
subject = getFirstValue(subjectFF);
}
try
{
final FormField userAmountFF = df.getField("num_users");
if (userAmountFF != null)
{
numusers = Integer.parseInt(getFirstValue(userAmountFF));
}
final FormField maxUsersFF = df.getField("num_max_users");
if (maxUsersFF != null)
{
numaxusers = Integer.parseInt(getFirstValue(maxUsersFF));
}
}
catch (NumberFormatException e)
{
reply.setError(PacketError.Condition.bad_request);
return reply;
}
final FormField includePasswordProtectedRoomsFF = df.getField("include_password_protected");
if (includePasswordProtectedRoomsFF != null)
{
final String b = getFirstValue(includePasswordProtectedRoomsFF);
if (b != null)
{
if (b.equals("0") || b.equalsIgnoreCase("false")
|| b.equalsIgnoreCase("no"))
{
includePasswordProtectedRooms = false;
}
}
}
// search for chatrooms matching the request params.
final List<MUCRoom> mucs = new ArrayList<MUCRoom>();
for (MUCRoom room : mucServer.getChatRooms())
{
boolean find = false;
if (names.size() > 0)
{
for (final String name : names)
{
if (name_is_exact_match)
{
if (name.equalsIgnoreCase(room.getNaturalLanguageName()))
{
find = true;
break;
}
}
else
{
if (room.getNaturalLanguageName().toLowerCase().indexOf(
name.toLowerCase()) != -1)
{
find = true;
break;
}
}
}
}
if (subject != null
&& room.getSubject().toLowerCase().indexOf(
subject.toLowerCase()) != -1)
{
find = true;
}
if (numusers > -1 && room.getParticipants().size() < numusers)
{
find = false;
}
if (numaxusers > -1 && room.getMaxUsers() < numaxusers)
{
find = false;
}
if (!includePasswordProtectedRooms && room.isPasswordProtected())
{
find = false;
}
if (find && canBeIncludedInResult(room))
{
mucs.add(room);
}
}
final ResultSet<MUCRoom> searchResults = new ResultSetImpl<MUCRoom>(
sortByUserAmount(mucs));
// See if the requesting entity would like to apply 'result set
// management'
final Element set = iq.getChildElement().element(
QName.get("set", ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
final List<MUCRoom> mucrsm;
// apply RSM only if the element exists, and the (total) results
// set is not empty.
final boolean applyRSM = set != null && !mucs.isEmpty();
if (applyRSM)
{
if (!ResultSet.isValidRSMRequest(set))
{
reply.setError(Condition.bad_request);
return reply;
}
try
{
mucrsm = searchResults.applyRSMDirectives(set);
}
catch (NullPointerException e)
{
final IQ itemNotFound = IQ.createResultIQ(iq);
itemNotFound.setError(Condition.item_not_found);
return itemNotFound;
}
}
else
{
// if no rsm, all found rooms are part of the result.
mucrsm = new ArrayList<MUCRoom>(searchResults);
}
ArrayList<XFormFieldImpl> fields = null;
final Element res = DocumentHelper.createElement(QName.get("query",
"jabber:iq:search"));
final XDataFormImpl resultform = new XDataFormImpl(DataForm.TYPE_RESULT);
boolean atLeastoneResult = false;
for (MUCRoom room : mucrsm)
{
fields = new ArrayList<XFormFieldImpl>();
XFormFieldImpl innerfield = new XFormFieldImpl("name");
innerfield.setType(FormField.TYPE_TEXT_SINGLE);
innerfield.addValue(room.getNaturalLanguageName());
fields.add(innerfield);
innerfield = new XFormFieldImpl("subject");
innerfield.setType(FormField.TYPE_TEXT_SINGLE);
innerfield.addValue(room.getSubject());
fields.add(innerfield);
innerfield = new XFormFieldImpl("num_users");
innerfield.setType(FormField.TYPE_TEXT_SINGLE);
innerfield.addValue(String.valueOf(room.getOccupantsCount()));
fields.add(innerfield);
innerfield = new XFormFieldImpl("num_max_users");
innerfield.setType(FormField.TYPE_TEXT_SINGLE);
innerfield.addValue(String.valueOf(room.getMaxUsers()));
fields.add(innerfield);
innerfield = new XFormFieldImpl("is_password_protected");
innerfield.setType(FormField.TYPE_BOOLEAN);
innerfield.addValue(Boolean.toString(room.isPasswordProtected()));
fields.add(innerfield);
innerfield = new XFormFieldImpl("is_member_only");
innerfield.setType(FormField.TYPE_BOOLEAN);
innerfield.addValue(Boolean.toString(room.isMembersOnly()));
fields.add(innerfield);
resultform.addItemFields(fields);
atLeastoneResult = true;
}
if (atLeastoneResult)
{
final FormField rffName = new XFormFieldImpl("name");
rffName.setLabel("Name");
resultform.addReportedField(rffName);
final FormField rffSubject = new XFormFieldImpl("subject");
rffSubject.setLabel("Subject");
resultform.addReportedField(rffSubject);
final FormField rffNumUsers = new XFormFieldImpl("num_users");
rffNumUsers.setLabel("Number of users");
resultform.addReportedField(rffNumUsers);
final FormField rffNumMaxUsers = new XFormFieldImpl("num_max_users");
rffNumMaxUsers.setLabel("Max number allowed of users");
resultform.addReportedField(rffNumMaxUsers);
final FormField rffPasswordProtected = new XFormFieldImpl(
"is_password_protected");
rffPasswordProtected.setLabel("Is a password protected room.");
resultform.addReportedField(rffPasswordProtected);
FormField innerfield = new XFormFieldImpl("is_member_only");
innerfield.setType(FormField.TYPE_TEXT_SINGLE);
innerfield.setLabel("Is a member only room.");
resultform.addReportedField(innerfield);
res.add(resultform.asXMLElement());
}
if (applyRSM)
{
res.add(searchResults.generateSetElementFromResults(mucrsm));
}
reply.setChildElement(res);
return reply;
}
/**
* Sorts the provided list in such a way that the MUC with the most users
* will be the first one in the list.
*
* @param mucs
* The unordered list that will be sorted.
*/
private static List<MUCRoom> sortByUserAmount(List<MUCRoom> mucs)
{
Collections.sort(mucs, new Comparator<MUCRoom>()
{
public int compare(MUCRoom o1, MUCRoom o2)
{
return o2.getOccupantsCount() - o1.getOccupantsCount();
}
});
return mucs;
}
/**
* Checks if the room may be included in search results. This is almost
* identical to {@link MultiUserChatServerImpl#canDiscoverRoom(Room room)},
* but that method is private and cannot be re-used here.
*
* @param room
* The room to check
* @return ''true'' if the room may be included in search results, ''false''
* otherwise.
*/
private static boolean canBeIncludedInResult(MUCRoom room)
{
// Check if locked rooms may be discovered
final boolean discoverLocked = Boolean.parseBoolean(JiveGlobals.getProperty(
"xmpp.muc.discover.locked", "true"));
if (!discoverLocked && room.isLocked())
{
return false;
}
return room.isPublicRoom();
}
/**
* Returns the first value from the FormField, or 'null' if no value has
* been set.
*
* @param formField
* The field from which to return the first value.
* @return String based value, or 'null' if the FormField has no values.
*/
public static String getFirstValue(FormField formField)
{
if (formField == null)
{
throw new IllegalArgumentException(
"The argument 'formField' cannot be null.");
}
Iterator<String> it = formField.getValues();
if (!it.hasNext())
{
return null;
}
return it.next();
}
}
......@@ -33,6 +33,7 @@ import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.openfire.stats.StatisticsManager;
import org.jivesoftware.util.*;
import org.jivesoftware.util.cache.CacheFactory;
import org.jivesoftware.util.resultsetmanager.ResultSet;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.*;
......@@ -53,8 +54,8 @@ import java.util.concurrent.atomic.AtomicLong;
*
* Temporary rooms are held in memory as long as they have occupants. They will be destroyed after
* the last occupant left the room. On the other hand, persistent rooms are always present in memory
* even after the last occupant left the room. In order to keep memory clean of peristent rooms that
* have been forgotten or abandonded this class includes a clean up process. The clean up process
* even after the last occupant left the room. In order to keep memory clean of persistent rooms that
* have been forgotten or abandoned this class includes a clean up process. The clean up process
* will remove from memory rooms that haven't had occupants for a while. Moreover, forgotten or
* abandoned rooms won't be loaded into memory when the Multi-User Chat service starts up.
*
......@@ -130,6 +131,12 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
* The handler of packets with namespace jabber:iq:register for the server.
*/
private IQMUCRegisterHandler registerHandler = null;
/**
* The handler of search requests ('jabber:iq:search' namespace).
*/
private IQMUCSearchHandler searchHandler = null;
/**
* The total time all agents took to chat *
*/
......@@ -187,7 +194,7 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
/**
* The time to elapse between each rooms cleanup. Default frequency is 60 minutes.
*/
private final long cleanup_frequency = 60 * 60 * 1000;
private static final long CLEANUP_FREQUENCY = 60 * 60 * 1000;
/**
* Total number of received messages in all rooms since the last reset. The counter
......@@ -273,6 +280,10 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
IQ reply = registerHandler.handleIQ(iq);
router.route(reply);
}
else if ("jabber:iq:search".equals(namespace)) {
IQ reply = searchHandler.handleIQ(iq);
router.route(reply);
}
else if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
// TODO MUC should have an IQDiscoInfoHandler of its own when MUC becomes
// a component
......@@ -439,7 +450,7 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
// Try to load the room's configuration from the database (if the room is
// persistent but was added to the DB after the server was started up or the
// room may be an old room that was not present in memory)
MUCPersistenceManager.loadFromDB((LocalMUCRoom) room);
MUCPersistenceManager.loadFromDB(room);
loaded = true;
}
catch (IllegalArgumentException e) {
......@@ -492,7 +503,7 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
// Try to load the room's configuration from the database (if the room is
// persistent but was added to the DB after the server was started up or the
// room may be an old room that was not present in memory)
MUCPersistenceManager.loadFromDB((LocalMUCRoom) room);
MUCPersistenceManager.loadFromDB(room);
loaded = true;
rooms.put(roomName, room);
}
......@@ -617,9 +628,9 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
}
/**
* Returns the limit date after which rooms whithout activity will be removed from memory.
* Returns the limit date after which rooms without activity will be removed from memory.
*
* @return the limit date after which rooms whithout activity will be removed from memory.
* @return the limit date after which rooms without activity will be removed from memory.
*/
private Date getCleanupDate() {
return new Date(System.currentTimeMillis() - (emptyLimit * 3600000));
......@@ -863,12 +874,14 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
timer.schedule(logConversationTask, log_timeout, log_timeout);
// Remove unused rooms from memory
cleanupTask = new CleanupTask();
timer.schedule(cleanupTask, cleanup_frequency, cleanup_frequency);
timer.schedule(cleanupTask, CLEANUP_FREQUENCY, CLEANUP_FREQUENCY);
routingTable = server.getRoutingTable();
router = server.getPacketRouter();
// Configure the handler of iq:register packets
registerHandler = new IQMUCRegisterHandler(this);
// Configure the handler of jabber:iq:search packets
searchHandler = new IQMUCSearchHandler(this);
// Listen to cluster events
ClusterManager.addListener(this);
}
......@@ -1109,8 +1122,13 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
identity.addAttribute("category", "conference");
identity.addAttribute("name", "Public Chatrooms");
identity.addAttribute("type", "text");
identities.add(identity);
Element searchId = DocumentHelper.createElement("identity");
searchId.addAttribute("category", "directory");
searchId.addAttribute("name", "Public Chatroom Search");
searchId.addAttribute("type", "chatroom");
identities.add(searchId);
}
else if (name != null && node == null) {
// Answer the identity of a given room
......@@ -1149,6 +1167,8 @@ public class MultiUserChatServerImpl extends BasicModule implements MultiUserCha
features.add("http://jabber.org/protocol/muc");
features.add("http://jabber.org/protocol/disco#info");
features.add("http://jabber.org/protocol/disco#items");
features.add("jabber:iq:search");
features.add(ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);
}
else if (name != null && node == null) {
// Answer the features of a given room
......
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