001: /**
002: * $Revision: $
003: * $Date: $
004: *
005: * Copyright (C) 2007 Jive Software. All rights reserved.
006: *
007: * This software is published under the terms of the GNU Public License (GPL),
008: * a copy of which is included in this distribution.
009: */package org.jivesoftware.openfire.muc.spi;
010:
011: import org.dom4j.DocumentHelper;
012: import org.dom4j.Element;
013: import org.dom4j.QName;
014: import org.jivesoftware.openfire.forms.DataForm;
015: import org.jivesoftware.openfire.forms.FormField;
016: import org.jivesoftware.openfire.forms.spi.XDataFormImpl;
017: import org.jivesoftware.openfire.forms.spi.XFormFieldImpl;
018: import org.jivesoftware.openfire.muc.MUCRoom;
019: import org.jivesoftware.openfire.muc.MultiUserChatServer;
020: import org.jivesoftware.openfire.resultsetmanager.ResultSet;
021: import org.jivesoftware.openfire.resultsetmanager.ResultSetImpl;
022: import org.jivesoftware.util.JiveGlobals;
023: import org.xmpp.packet.IQ;
024: import org.xmpp.packet.PacketError;
025: import org.xmpp.packet.PacketError.Condition;
026:
027: import java.util.*;
028:
029: /**
030: * This class adds jabber:iq:search combined with 'result set management'
031: * functionality to the MUC service of Openfire.
032: *
033: * @author Guus der Kinderen - Nimbuzz B.V. <guus@nimbuzz.com>
034: * @author Giancarlo Frison - Nimbuzz B.V. <giancarlo@nimbuzz.com>
035: */
036: public class IQMUCSearchHandler {
037: /**
038: * The MUC-server to extend with jabber:iq:search functionality.
039: */
040: private final MultiUserChatServer mucServer;
041:
042: /**
043: * Creates a new instance of the search provider.
044: *
045: * @param mucServer
046: * The server for which to return search results.
047: */
048: public IQMUCSearchHandler(MultiUserChatServer mucServer) {
049: this .mucServer = mucServer;
050: }
051:
052: /**
053: * Utility method that returns a 'jabber:iq:search' child element filled
054: * with a blank dataform.
055: *
056: * @return Element, named 'query', escaped by the 'jabber:iq:search'
057: * namespace, filled with a blank dataform.
058: */
059: private static Element getDataElement() {
060: final XDataFormImpl searchForm = new XDataFormImpl(
061: DataForm.TYPE_FORM);
062: searchForm.setTitle("Chat Rooms Search");
063: searchForm.addInstruction("Instructions");
064:
065: final FormField typeFF = new XFormFieldImpl("FORM_TYPE");
066: typeFF.setType(FormField.TYPE_HIDDEN);
067: typeFF.addValue("jabber:iq:search");
068: searchForm.addField(typeFF);
069:
070: final FormField nameFF = new XFormFieldImpl("name");
071: nameFF.setType(FormField.TYPE_TEXT_SINGLE);
072: nameFF.setLabel("Name");
073: nameFF.setRequired(false);
074: searchForm.addField(nameFF);
075:
076: final FormField matchFF = new XFormFieldImpl(
077: "name_is_exact_match");
078: matchFF.setType(FormField.TYPE_BOOLEAN);
079: matchFF.setLabel("Name must match exactly");
080: matchFF.setRequired(false);
081: searchForm.addField(matchFF);
082:
083: final FormField subjectFF = new XFormFieldImpl("subject");
084: subjectFF.setType(FormField.TYPE_TEXT_SINGLE);
085: subjectFF.setLabel("Subject");
086: subjectFF.setRequired(false);
087: searchForm.addField(subjectFF);
088:
089: final FormField userAmountFF = new XFormFieldImpl("num_users");
090: userAmountFF.setType(FormField.TYPE_TEXT_SINGLE);
091: userAmountFF.setLabel("Number of users");
092: userAmountFF.setRequired(false);
093: searchForm.addField(userAmountFF);
094:
095: final FormField maxUsersFF = new XFormFieldImpl("num_max_users");
096: maxUsersFF.setType(FormField.TYPE_TEXT_SINGLE);
097: maxUsersFF.setLabel("Max number allowed of users");
098: maxUsersFF.setRequired(false);
099: searchForm.addField(maxUsersFF);
100:
101: final FormField includePasswordProtectedFF = new XFormFieldImpl(
102: "include_password_protected");
103: includePasswordProtectedFF.setType(FormField.TYPE_BOOLEAN);
104: includePasswordProtectedFF
105: .setLabel("Include password protected rooms");
106: includePasswordProtectedFF.setRequired(false);
107: searchForm.addField(includePasswordProtectedFF);
108:
109: final Element probeResult = DocumentHelper.createElement(QName
110: .get("query", "jabber:iq:search"));
111: probeResult.add(searchForm.asXMLElement());
112: return probeResult;
113: }
114:
115: /**
116: * Constructs an answer on a IQ stanza that contains a search request. The
117: * answer will be an IQ stanza of type 'result' or 'error'.
118: *
119: * @param iq
120: * The IQ stanza that is the search request.
121: * @return An answer to the provided request.
122: */
123: public IQ handleIQ(IQ iq) {
124: final IQ reply = IQ.createResultIQ(iq);
125: final Element formElement = iq.getChildElement().element(
126: QName.get("x", "jabber:x:data"));
127: if (formElement == null) {
128: reply.setChildElement(getDataElement());
129: return reply;
130: }
131:
132: // parse params from request.
133: final XDataFormImpl df = new XDataFormImpl();
134: df.parse(formElement);
135: boolean name_is_exact_match = false;
136: String subject = null;
137: int numusers = -1;
138: int numaxusers = -1;
139: boolean includePasswordProtectedRooms = true;
140:
141: final Set<String> names = new HashSet<String>();
142: final Iterator<FormField> formFields = df.getFields();
143: while (formFields.hasNext()) {
144:
145: final FormField field = formFields.next();
146: if (field.getVariable().equals("name")) {
147: names.add(getFirstValue(field));
148: }
149: }
150:
151: final FormField matchFF = df.getField("name_is_exact_match");
152: if (matchFF != null) {
153: final String b = getFirstValue(matchFF);
154: if (b != null) {
155: name_is_exact_match = b.equals("1")
156: || b.equalsIgnoreCase("true")
157: || b.equalsIgnoreCase("yes");
158: }
159: }
160:
161: final FormField subjectFF = df.getField("subject");
162: if (subjectFF != null) {
163: subject = getFirstValue(subjectFF);
164: }
165:
166: try {
167: final FormField userAmountFF = df.getField("num_users");
168: if (userAmountFF != null) {
169: String value = getFirstValue(userAmountFF);
170: if (value != null && !"".equals(value)) {
171: numusers = Integer.parseInt(value);
172: }
173: }
174:
175: final FormField maxUsersFF = df.getField("num_max_users");
176: if (maxUsersFF != null) {
177: String value = getFirstValue(maxUsersFF);
178: if (value != null && !"".equals(value)) {
179: numaxusers = Integer.parseInt(value);
180: }
181: }
182: } catch (NumberFormatException e) {
183: reply.setError(PacketError.Condition.bad_request);
184: return reply;
185: }
186:
187: final FormField includePasswordProtectedRoomsFF = df
188: .getField("include_password_protected");
189: if (includePasswordProtectedRoomsFF != null) {
190: final String b = getFirstValue(includePasswordProtectedRoomsFF);
191: if (b != null) {
192: if (b.equals("0") || b.equalsIgnoreCase("false")
193: || b.equalsIgnoreCase("no")) {
194: includePasswordProtectedRooms = false;
195: }
196: }
197: }
198:
199: // search for chatrooms matching the request params.
200: final List<MUCRoom> mucs = new ArrayList<MUCRoom>();
201: for (MUCRoom room : mucServer.getChatRooms()) {
202: boolean find = false;
203:
204: if (names.size() > 0) {
205: for (final String name : names) {
206: if (name_is_exact_match) {
207: if (name.equalsIgnoreCase(room
208: .getNaturalLanguageName())) {
209: find = true;
210: break;
211: }
212: } else {
213: if (room.getNaturalLanguageName().toLowerCase()
214: .indexOf(name.toLowerCase()) != -1) {
215: find = true;
216: break;
217: }
218: }
219: }
220: }
221:
222: if (subject != null
223: && room.getSubject().toLowerCase().indexOf(
224: subject.toLowerCase()) != -1) {
225: find = true;
226: }
227:
228: if (numusers > -1
229: && room.getParticipants().size() < numusers) {
230: find = false;
231: }
232:
233: if (numaxusers > -1 && room.getMaxUsers() < numaxusers) {
234: find = false;
235: }
236:
237: if (!includePasswordProtectedRooms
238: && room.isPasswordProtected()) {
239: find = false;
240: }
241:
242: if (find && canBeIncludedInResult(room)) {
243: mucs.add(room);
244: }
245: }
246:
247: final ResultSet<MUCRoom> searchResults = new ResultSetImpl<MUCRoom>(
248: sortByUserAmount(mucs));
249:
250: // See if the requesting entity would like to apply 'result set
251: // management'
252: final Element set = iq.getChildElement().element(
253: QName.get("set",
254: ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
255: final List<MUCRoom> mucrsm;
256:
257: // apply RSM only if the element exists, and the (total) results
258: // set is not empty.
259: final boolean applyRSM = set != null && !mucs.isEmpty();
260:
261: if (applyRSM) {
262: if (!ResultSet.isValidRSMRequest(set)) {
263: reply.setError(Condition.bad_request);
264: return reply;
265: }
266:
267: try {
268: mucrsm = searchResults.applyRSMDirectives(set);
269: } catch (NullPointerException e) {
270: final IQ itemNotFound = IQ.createResultIQ(iq);
271: itemNotFound.setError(Condition.item_not_found);
272: return itemNotFound;
273: }
274: } else {
275: // if no rsm, all found rooms are part of the result.
276: mucrsm = new ArrayList<MUCRoom>(searchResults);
277: }
278:
279: ArrayList<XFormFieldImpl> fields = null;
280: final Element res = DocumentHelper.createElement(QName.get(
281: "query", "jabber:iq:search"));
282:
283: final XDataFormImpl resultform = new XDataFormImpl(
284: DataForm.TYPE_RESULT);
285: boolean atLeastoneResult = false;
286: for (MUCRoom room : mucrsm) {
287: fields = new ArrayList<XFormFieldImpl>();
288: XFormFieldImpl innerfield = new XFormFieldImpl("name");
289: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
290: innerfield.addValue(room.getNaturalLanguageName());
291: fields.add(innerfield);
292: innerfield = new XFormFieldImpl("subject");
293: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
294: innerfield.addValue(room.getSubject());
295: fields.add(innerfield);
296: innerfield = new XFormFieldImpl("num_users");
297: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
298: innerfield.addValue(String
299: .valueOf(room.getOccupantsCount()));
300: fields.add(innerfield);
301: innerfield = new XFormFieldImpl("num_max_users");
302: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
303: innerfield.addValue(String.valueOf(room.getMaxUsers()));
304: fields.add(innerfield);
305: innerfield = new XFormFieldImpl("is_password_protected");
306: innerfield.setType(FormField.TYPE_BOOLEAN);
307: innerfield.addValue(Boolean.toString(room
308: .isPasswordProtected()));
309: fields.add(innerfield);
310: innerfield = new XFormFieldImpl("is_member_only");
311: innerfield.setType(FormField.TYPE_BOOLEAN);
312: innerfield.addValue(Boolean.toString(room.isMembersOnly()));
313: fields.add(innerfield);
314: innerfield = new XFormFieldImpl("jid");
315: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
316: innerfield.addValue(room.getRole().getRoleAddress()
317: .toString());
318: fields.add(innerfield);
319: resultform.addItemFields(fields);
320: atLeastoneResult = true;
321: }
322: if (atLeastoneResult) {
323: final FormField rffName = new XFormFieldImpl("name");
324: rffName.setLabel("Name");
325: resultform.addReportedField(rffName);
326:
327: final FormField rffSubject = new XFormFieldImpl("subject");
328: rffSubject.setLabel("Subject");
329: resultform.addReportedField(rffSubject);
330:
331: final FormField rffNumUsers = new XFormFieldImpl(
332: "num_users");
333: rffNumUsers.setLabel("Number of users");
334: resultform.addReportedField(rffNumUsers);
335:
336: final FormField rffNumMaxUsers = new XFormFieldImpl(
337: "num_max_users");
338: rffNumMaxUsers.setLabel("Max number allowed of users");
339: resultform.addReportedField(rffNumMaxUsers);
340:
341: final FormField rffPasswordProtected = new XFormFieldImpl(
342: "is_password_protected");
343: rffPasswordProtected
344: .setLabel("Is a password protected room.");
345: resultform.addReportedField(rffPasswordProtected);
346:
347: final FormField rffJID = new XFormFieldImpl("jid");
348: rffJID.setLabel("JID");
349: resultform.addReportedField(rffJID);
350:
351: FormField innerfield = new XFormFieldImpl("is_member_only");
352: innerfield.setType(FormField.TYPE_TEXT_SINGLE);
353: innerfield.setLabel("Is a member only room.");
354: resultform.addReportedField(innerfield);
355: res.add(resultform.asXMLElement());
356: }
357:
358: if (applyRSM) {
359: res
360: .add(searchResults
361: .generateSetElementFromResults(mucrsm));
362: }
363:
364: reply.setChildElement(res);
365:
366: return reply;
367: }
368:
369: /**
370: * Sorts the provided list in such a way that the MUC with the most users
371: * will be the first one in the list.
372: *
373: * @param mucs
374: * The unordered list that will be sorted.
375: */
376: private static List<MUCRoom> sortByUserAmount(List<MUCRoom> mucs) {
377: Collections.sort(mucs, new Comparator<MUCRoom>() {
378: public int compare(MUCRoom o1, MUCRoom o2) {
379: return o2.getOccupantsCount() - o1.getOccupantsCount();
380: }
381: });
382:
383: return mucs;
384: }
385:
386: /**
387: * Checks if the room may be included in search results. This is almost
388: * identical to {@link MultiUserChatServerImpl#canDiscoverRoom(MUCRoom room)},
389: * but that method is private and cannot be re-used here.
390: *
391: * @param room
392: * The room to check
393: * @return ''true'' if the room may be included in search results, ''false''
394: * otherwise.
395: */
396: private static boolean canBeIncludedInResult(MUCRoom room) {
397: // Check if locked rooms may be discovered
398: final boolean discoverLocked = JiveGlobals.getBooleanProperty(
399: "xmpp.muc.discover.locked", true);
400:
401: if (!discoverLocked && room.isLocked()) {
402: return false;
403: }
404: return room.isPublicRoom();
405: }
406:
407: /**
408: * Returns the first value from the FormField, or 'null' if no value has
409: * been set.
410: *
411: * @param formField
412: * The field from which to return the first value.
413: * @return String based value, or 'null' if the FormField has no values.
414: */
415: public static String getFirstValue(FormField formField) {
416: if (formField == null) {
417: throw new IllegalArgumentException(
418: "The argument 'formField' cannot be null.");
419: }
420:
421: Iterator<String> it = formField.getValues();
422:
423: if (!it.hasNext()) {
424: return null;
425: }
426:
427: return it.next();
428: }
429: }
|