diff --git a/pom.xml b/pom.xml
index 3c3750a5..a0f26dc0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.buddycloud
channelserver
jar
- 0.12.0
+ 0.12.0-search
Buddycloud Java Server
https://github.com/buddycloud/buddycloud-server-java
diff --git a/src/main/java/org/buddycloud/channelserver/channel/ChannelManager.java b/src/main/java/org/buddycloud/channelserver/channel/ChannelManager.java
index 78645e8f..02a58e14 100644
--- a/src/main/java/org/buddycloud/channelserver/channel/ChannelManager.java
+++ b/src/main/java/org/buddycloud/channelserver/channel/ChannelManager.java
@@ -1,8 +1,12 @@
package org.buddycloud.channelserver.channel;
+import java.util.List;
+
+import org.buddycloud.channelserver.db.CloseableIterator;
import org.buddycloud.channelserver.db.NodeStore;
import org.buddycloud.channelserver.db.exception.NodeStoreException;
import org.buddycloud.channelserver.pubsub.affiliation.Affiliations;
+import org.buddycloud.channelserver.pubsub.model.NodeItem;
import org.xmpp.packet.JID;
public interface ChannelManager extends NodeStore {
@@ -47,9 +51,25 @@ public interface ChannelManager extends NodeStore {
/**
* Gets the default affiliation for a node
- * @return
+ *
+ * @return
*
* @throws NodeStoreException
*/
- Affiliations getDefaultNodeAffiliation(String nodeId) throws NodeStoreException;
+ Affiliations getDefaultNodeAffiliation(String nodeId)
+ throws NodeStoreException;
+
+ /**
+ * Searches for the provided content or author, or both, across nodes
+ * the searcher has access to
+ *
+ * @param searcher
+ * @param content
+ * @param author
+ * @param page
+ * @param rpp
+ * @return
+ */
+ CloseableIterator performSearch(JID searcher, List content, JID author, int page,
+ int rpp) throws NodeStoreException;
}
\ No newline at end of file
diff --git a/src/main/java/org/buddycloud/channelserver/channel/ChannelManagerImpl.java b/src/main/java/org/buddycloud/channelserver/channel/ChannelManagerImpl.java
index 0f1710e2..86faf947 100644
--- a/src/main/java/org/buddycloud/channelserver/channel/ChannelManagerImpl.java
+++ b/src/main/java/org/buddycloud/channelserver/channel/ChannelManagerImpl.java
@@ -2,6 +2,7 @@
import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -444,6 +445,12 @@ public Affiliations getDefaultNodeAffiliation(String nodeId)
return Affiliations.member;
}
+ @Override
+ public CloseableIterator performSearch(JID searcher,
+ List content, JID author, int page, int rpp) throws NodeStoreException {
+ return nodeStore.performSearch(searcher, content, author, page, rpp);
+ }
+
@Override
public ResultSet getUserItems(JID userJid) throws NodeStoreException {
return nodeStore.getUserItems(userJid);
@@ -473,5 +480,6 @@ public ResultSet getNodeThreads(String node, String afterId,
@Override
public int countNodeThreads(String node) throws NodeStoreException {
return nodeStore.countNodeThreads(node);
- }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/buddycloud/channelserver/db/NodeStore.java b/src/main/java/org/buddycloud/channelserver/db/NodeStore.java
index f3b1b079..b9906ab2 100644
--- a/src/main/java/org/buddycloud/channelserver/db/NodeStore.java
+++ b/src/main/java/org/buddycloud/channelserver/db/NodeStore.java
@@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import org.buddycloud.channelserver.db.exception.NodeStoreException;
@@ -558,6 +559,20 @@ void deleteNodeItemById(String nodeId, String nodeItemId)
*/
ArrayList getNodeList() throws NodeStoreException;
+ /**
+ * Search subscribed nodes for content
+ *
+ * @param searcher JID of user performing the search
+ * @param content Keywords upon which to search
+ * @param author JID of the content author
+ * @param page Page number of results (>= 1)
+ * @param rpp Results per page (>= 1)
+ * @return
+ * @throws NodeStoreException
+ */
+ CloseableIterator performSearch(JID searcher, List content,
+ JID author, int page, int rpp) throws NodeStoreException;
+
/**
* Retrieves a list of items from public channels "firehose"
*
diff --git a/src/main/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStore.java b/src/main/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStore.java
index b489eac1..e3a3d083 100644
--- a/src/main/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStore.java
+++ b/src/main/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStore.java
@@ -11,6 +11,7 @@
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -1532,6 +1533,66 @@ public void purgeNodeItems(String nodeId) throws NodeStoreException {
close(stmt); // Will implicitly close the resultset if required
}
}
+
+ @Override
+ public CloseableIterator performSearch(JID searcher,
+ List content, JID author, int page, int rpp) throws NodeStoreException {
+ PreparedStatement stmt = null;
+ try {
+ String sql = "SELECT * FROM \"items\" " +
+ "LEFT JOIN \"subscriptions\" ON \"items\".\"node\" = \"subscriptions\".\"node\" " +
+ "WHERE " +
+ "\"subscriptions\".\"user\" = ? " +
+ "AND \"subscriptions\".\"subscription\" = 'subscribed' " +
+ "AND RIGHT(\"items\".\"node\", 6) = '/posts' " +
+ " $searchParameters " +
+ "ORDER BY \"items\".\"updated\" DESC " +
+ "LIMIT ? OFFSET ?;";
+ ArrayList parameterValues = new ArrayList();
+ parameterValues.add(searcher.toBareJID());
+ String searchParameters = "";
+
+ for (String term : (List) content) {
+ searchParameters += "AND (\"items\".\"xml\" LIKE ?) ";
+ parameterValues.add("%%" + term + "%%");
+ }
+
+ if (null != author) {
+ searchParameters += "AND (\"items\".\"xml\" LIKE ?)";
+ parameterValues.add("%" + author.toBareJID() + "%");
+ }
+ stmt = conn.prepareStatement(sql.replace("$searchParameters", searchParameters));
+
+ int counter = 0;
+ for (String value : parameterValues) {
+ ++counter;
+ stmt.setString(counter, value);
+ }
+ stmt.setInt(parameterValues.size() + 1, rpp);
+ stmt.setInt(parameterValues.size() + 2, (page - 1) * rpp);
+
+ java.sql.ResultSet rs = stmt.executeQuery();
+
+ stmt = null; // Prevent the finally block from closing the
+ // statement
+
+ return new ResultSetIterator(rs,
+ new ResultSetIterator.RowConverter() {
+ @Override
+ public NodeItem convertRow(java.sql.ResultSet rs)
+ throws SQLException {
+ return new NodeItemImpl(rs.getString(1),
+ rs.getString(2), rs.getTimestamp(3),
+ rs.getString(4), rs.getString(5));
+ }
+ });
+ } catch (SQLException e) {
+ e.printStackTrace();
+ throw new NodeStoreException(e);
+ } finally {
+ close(stmt); // Will implicitly close the resultset if required
+ }
+ }
@Override
public ArrayList getNodeList() throws NodeStoreException {
diff --git a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/IQProcessor.java b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/IQProcessor.java
index 1a29770d..22c59fe1 100644
--- a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/IQProcessor.java
+++ b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/IQProcessor.java
@@ -13,6 +13,7 @@
import org.buddycloud.channelserver.packetprocessor.iq.namespace.mam.MessageArchiveManagement;
import org.buddycloud.channelserver.packetprocessor.iq.namespace.pubsub.JabberPubsub;
import org.buddycloud.channelserver.packetprocessor.iq.namespace.register.JabberRegister;
+import org.buddycloud.channelserver.packetprocessor.iq.namespace.search.Search;
import org.buddycloud.channelserver.queue.FederatedQueueManager;
import org.buddycloud.channelserver.queue.UnknownFederatedPacketException;
import org.xmpp.packet.IQ;
@@ -49,6 +50,8 @@ public IQProcessor(BlockingQueue outQueue, Configuration conf,
processorsPerNamespace.put(JabberPubsub.NS_PUBSUB_OWNER, ps);
processorsPerNamespace.put(MessageArchiveManagement.NAMESPACE_MAM,
new MessageArchiveManagement(outQueue, channelManager));
+ processorsPerNamespace.put(Search.NAMESPACE_URI,
+ new Search(outQueue, channelManager));
}
@Override
diff --git a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/discoinfo/DiscoInfoGet.java b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/discoinfo/DiscoInfoGet.java
index 138999f8..3f279b71 100644
--- a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/discoinfo/DiscoInfoGet.java
+++ b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/discoinfo/DiscoInfoGet.java
@@ -129,6 +129,13 @@ private void sendServerDiscoInfo()
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/disco#info");
+
+ query.addElement("feature").addAttribute("var",
+ "http://jabber.org/protocol/disco#items");
+
+ query.addElement("feature").addAttribute("var",
+ "jabber:iq:search");
+
outQueue.put(result);
}
diff --git a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/Search.java b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/Search.java
new file mode 100644
index 00000000..590b243f
--- /dev/null
+++ b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/Search.java
@@ -0,0 +1,68 @@
+package org.buddycloud.channelserver.packetprocessor.iq.namespace.search;
+
+import java.util.Collection;
+import java.util.concurrent.BlockingQueue;
+
+import org.apache.log4j.Logger;
+import org.buddycloud.channelserver.Configuration;
+import org.buddycloud.channelserver.channel.ChannelManager;
+import org.buddycloud.channelserver.channel.Conf;
+import org.buddycloud.channelserver.db.exception.NodeStoreException;
+import org.buddycloud.channelserver.packetprocessor.PacketProcessor;
+import org.buddycloud.channelserver.packetprocessor.iq.namespace.pubsub.JabberPubsub;
+import org.buddycloud.channelserver.pubsub.accessmodel.AccessModels;
+import org.buddycloud.channelserver.pubsub.model.impl.NodeSubscriptionImpl;
+import org.buddycloud.channelserver.pubsub.subscription.Subscriptions;
+import org.dom4j.Element;
+import org.dom4j.QName;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.IQ.Type;
+import org.xmpp.packet.JID;
+import org.xmpp.packet.Packet;
+import org.xmpp.packet.PacketError;
+
+public class Search implements PacketProcessor {
+
+ public static final String ELEMENT_NAME = "query";
+ private static final Logger logger = Logger.getLogger(Search.class);
+
+ public static final String NAMESPACE_URI = "jabber:iq:search";
+
+ private final BlockingQueue outQueue;
+ private final ChannelManager channelManager;
+ private IQ request;
+
+ private SearchGet searchGet;
+ private SearchSet searchSet;
+
+ public Search(BlockingQueue outQueue,
+ ChannelManager channelManager) {
+ this.outQueue = outQueue;
+ this.channelManager = channelManager;
+
+ this.searchGet = new SearchGet(outQueue, channelManager);
+ this.searchSet = new SearchSet(outQueue, channelManager);
+ }
+
+ @Override
+ public void process(IQ reqIQ) throws Exception {
+ request = reqIQ;
+ if (request.getType().equals(Type.get)) {
+ logger.trace("Using search processor: SearchGet");
+ this.searchGet.process(request);
+ return;
+ } else if (request.getType().equals(Type.set)) {
+ logger.trace("Using search processor: SearchSet");
+ this.searchSet.process(request);
+ return;
+ }
+ IQ response = IQ.createResultIQ(request);
+ response.setType(IQ.Type.error);
+ PacketError error = new PacketError(
+ PacketError.Condition.bad_request,
+ PacketError.Type.modify
+ );
+ response.setError(error);
+ outQueue.put(response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGet.java b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGet.java
new file mode 100644
index 00000000..60258d5b
--- /dev/null
+++ b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGet.java
@@ -0,0 +1,94 @@
+package org.buddycloud.channelserver.packetprocessor.iq.namespace.search;
+
+import java.util.concurrent.BlockingQueue;
+
+import org.buddycloud.channelserver.channel.ChannelManager;
+import org.buddycloud.channelserver.packetprocessor.PacketProcessor;
+import org.dom4j.Element;
+import org.xmpp.forms.DataForm;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.Packet;
+import org.xmpp.packet.PacketError;
+import org.xmpp.packet.PacketError.Type;
+
+public class SearchGet implements PacketProcessor {
+
+ public static final String INSTRUCTIONS = "Search for content/hashtags/mentions";
+
+ public static final String TITLE = "Please populate one or more of the following fields";
+ public static final String CONTENT_FIELD_LABEL = "Content search";
+ public static final String AUTHOR_FIELD_LABEL = "Author";
+ public static final String RPP_FIELD_LABEL = "Results per page";
+ public static final String PAGE_FIELD_LABEL = "Page";
+
+ private ChannelManager channelManager;
+ private BlockingQueue outQueue;
+ private IQ response;
+
+ private Element x;
+
+ public SearchGet(BlockingQueue outQueue,
+ ChannelManager channelManager) {
+ this.channelManager = channelManager;
+ this.outQueue = outQueue;
+ }
+
+ @Override
+ public void process(IQ request) throws Exception {
+ response = IQ.createResultIQ(request);
+
+ if (false == channelManager.isLocalJID(request.getFrom())) {
+ sendErrorResponse(PacketError.Type.cancel,
+ PacketError.Condition.not_allowed);
+ return;
+ }
+
+ Element query = response.getElement().addElement("query");
+ query.addAttribute("xmlns", Search.NAMESPACE_URI);
+ query.addElement("instructions").addText(INSTRUCTIONS);
+ x = query.addElement("x");
+ addFields();
+ outQueue.put(response);
+ }
+
+ private void addFields() {
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addElement("title").addText(TITLE);
+ x.addElement("instructions").addText(INSTRUCTIONS);
+
+ Element formType = x.addElement("field");
+ formType.addAttribute("type", "hidden");
+ formType.addAttribute("var", "FORM_TYPE");
+ formType.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element content = x.addElement("field");
+ content.addAttribute("type", "text-multi");
+ content.addAttribute("var", "content");
+ content.addAttribute("label", CONTENT_FIELD_LABEL);
+
+ Element author = x.addElement("field");
+ author.addAttribute("type", "jid-single");
+ author.addAttribute("var", "author");
+ author.addAttribute("label", AUTHOR_FIELD_LABEL);
+
+ Element rpp = x.addElement("field");
+ rpp.addAttribute("type", "fixed");
+ rpp.addAttribute("var", "rpp");
+ rpp.addAttribute("label", RPP_FIELD_LABEL);
+
+ Element page = x.addElement("field");
+ page.addAttribute("type", "fixed");
+ page.addAttribute("var", "page");
+ page.addAttribute("label", PAGE_FIELD_LABEL);
+ }
+
+ private void sendErrorResponse(PacketError.Type type,
+ PacketError.Condition condition) throws InterruptedException {
+ response.setType(IQ.Type.error);
+ PacketError error = new PacketError(PacketError.Condition.not_allowed,
+ PacketError.Type.cancel);
+ response.setError(error);
+ outQueue.put(response);
+ }
+
+}
diff --git a/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSet.java b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSet.java
new file mode 100644
index 00000000..d6539c75
--- /dev/null
+++ b/src/main/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSet.java
@@ -0,0 +1,296 @@
+package org.buddycloud.channelserver.packetprocessor.iq.namespace.search;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+
+import org.apache.log4j.Logger;
+import org.buddycloud.channelserver.channel.ChannelManager;
+import org.buddycloud.channelserver.db.CloseableIterator;
+import org.buddycloud.channelserver.db.exception.NodeStoreException;
+import org.buddycloud.channelserver.packetprocessor.PacketProcessor;
+import org.buddycloud.channelserver.pubsub.model.NodeItem;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.dom4j.tree.DefaultElement;
+import org.xmpp.forms.DataForm;
+import org.xmpp.forms.FormField;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.JID;
+import org.xmpp.packet.Packet;
+import org.xmpp.packet.PacketError;
+
+public class SearchSet implements PacketProcessor {
+
+ private ChannelManager channelManager;
+ private BlockingQueue outQueue;
+ private IQ responseIq;
+ private Element x;
+ private Element query;
+ private IQ requestIq;
+ private ArrayList content = new ArrayList();
+ private int page = 1;
+ private int rpp = 25;
+ private JID author;
+ private JID searcher;
+
+ public static Logger logger = Logger.getLogger(SearchSet.class);
+
+ public SearchSet(BlockingQueue outQueue,
+ ChannelManager channelManager) {
+ this.channelManager = channelManager;
+ this.outQueue = outQueue;
+ }
+
+ @Override
+ public void process(IQ request) throws Exception {
+ searcher = request.getFrom();
+ responseIq = IQ.createResultIQ(request);
+ this.requestIq = request;
+
+ if (false == isValidRequest()) {
+ return;
+ }
+
+ if (false == processForm()) {
+ return;
+ }
+
+ try {
+ runSearch();
+ } catch (NodeStoreException e) {
+ sendErrorResponse(PacketError.Type.wait,
+ PacketError.Condition.internal_server_error);
+ return;
+ }
+
+ outQueue.put(responseIq);
+ }
+
+ private boolean isValidRequest() throws Exception {
+
+ if (false == channelManager.isLocalJID(searcher)) {
+ sendErrorResponse(PacketError.Type.cancel,
+ PacketError.Condition.not_allowed);
+ return false;
+ }
+
+ if (false == hasDataForm() || false == dataFormCorrect()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean hasDataForm() throws Exception {
+ x = requestIq.getElement().element("query").element("x");
+
+ if (null == x || !DataForm.NAMESPACE.equals(x.getNamespaceURI())
+ || !"submit".equals(x.attributeValue("type"))) {
+ sendErrorResponse(PacketError.Type.modify,
+ PacketError.Condition.bad_request);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean dataFormCorrect() throws Exception {
+ if (!hasCorrectFormElement() || !hasEnoughFormFields()) {
+ sendErrorResponse(PacketError.Type.modify,
+ PacketError.Condition.bad_request);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean hasCorrectFormElement() throws Exception {
+
+ List elements = x.elements("field");
+
+ if (elements.size() > 0) {
+ for (Element field : elements) {
+ if (!"FORM_TYPE".equals(field.attributeValue("var"))) {
+ continue;
+ }
+
+ String value = field.elementText("value");
+
+ if (null == value || !Search.NAMESPACE_URI.equals(value)) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean hasEnoughFormFields() throws Exception {
+ List elements = x.elements("field");
+ if (elements.size() < 2) {
+ return false;
+ }
+
+ boolean hasContentOrAuthor = false;
+
+ String var;
+ for (Element field : elements) {
+ var = field.attributeValue("var");
+ if ("content".equals(var) || "author".equals(var)) {
+ hasContentOrAuthor = true;
+ }
+ }
+
+ return hasContentOrAuthor;
+ }
+
+ private boolean processForm() throws Exception {
+ try {
+ extractFieldValues();
+ } catch (NumberFormatException e) {
+ return false;
+ }
+
+ if (false == checkFieldValues()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void runSearch() throws NodeStoreException {
+ CloseableIterator results = channelManager.performSearch(
+ searcher, content, author, page, rpp);
+
+ Element query = responseIq.getElement().addElement("query");
+ query.addAttribute("xmlns", Search.NAMESPACE_URI);
+
+ Element x = new DefaultElement("x");
+ int resultCounter = 0;
+ NodeItem nodeItem;
+ Element entry;
+
+ SAXReader xmlReader = new SAXReader();
+ while (results.hasNext()) {
+ if (0 == resultCounter) {
+ addFormField(x);
+ addReportedFields(x);
+ }
+
+ nodeItem = results.next();
+
+ try {
+ entry = xmlReader.read(new StringReader(nodeItem.getPayload()))
+ .getRootElement();
+
+ Element item = x.addElement("item");
+
+ item.addElement("field").addAttribute("var", "node")
+ .addElement("value").setText(nodeItem.getNodeId());
+
+ item.addElement("field").addAttribute("var", "id")
+ .addElement("value").setText(nodeItem.getId());
+
+ item.addElement("field").addAttribute("var", "entry")
+ .addElement("value").add(entry);
+ } catch (DocumentException e) {
+ logger.error("Error parsing a node entry, ignoring. "
+ + nodeItem);
+ }
+
+ resultCounter++;
+ }
+
+ if (resultCounter > 0) {
+ query.add(x);
+ }
+
+ }
+
+ private void addFormField(Element x) {
+ x.addElement("field").addAttribute("type", "hidden")
+ .addAttribute("var", "FORM_TYPE").addElement("value")
+ .setText(Search.NAMESPACE_URI);
+ }
+
+ private void addReportedFields(Element x) {
+ Element reported = x.addElement("reported");
+
+ reported.addElement("field").addAttribute("var", "node")
+ .addAttribute("label", "Node")
+ .addAttribute("type", "text-single");
+
+ reported.addElement("field").addAttribute("var", "id")
+ .addAttribute("label", "Item ID")
+ .addAttribute("type", "text-single");
+
+ reported.addElement("field").addAttribute("var", "entry")
+ .addAttribute("label", "Item").addAttribute("type", "xml");
+ }
+
+ private void extractFieldValues() {
+ List elements = x.elements("field");
+ String var;
+ for (Element field : elements) {
+ var = field.attributeValue("var");
+ if ("content".equals(var)) {
+ content = getValuesAsList(field);
+ } else if ("author".equals(var)) {
+ String authorStr = field.elementText("value");
+ if (authorStr.length() > 0) {
+ author = new JID(authorStr);
+ }
+ } else if ("page".equals(var)) {
+ page = getValueAsNumber(field);
+ } else if ("rpp".equals(var)) {
+ rpp = getValueAsNumber(field);
+ }
+ }
+ }
+
+ private boolean checkFieldValues() throws Exception {
+ if (((null != content && content.size() > 0) || (null != author && author
+ .toBareJID().length() > 0)) && (page > 0 && rpp > 0)) {
+ return true;
+ }
+
+ sendErrorResponse(PacketError.Type.modify,
+ PacketError.Condition.bad_request);
+ return false;
+
+ }
+
+ private ArrayList getValuesAsList(Element field) {
+ ArrayList rtn = new ArrayList();
+ String valueText;
+ for (Element value : (List) field.elements("value")) {
+ valueText = value.getText();
+ if (valueText.length() == 0) {
+ continue;
+ }
+ rtn.add(valueText);
+ }
+ return rtn;
+ }
+
+ private Integer getValueAsNumber(Element field)
+ throws NumberFormatException {
+ String valueStr = field.elementText("value");
+ return Integer.parseInt(valueStr);
+ }
+
+ private void sendErrorResponse(PacketError.Type type,
+ PacketError.Condition condition) throws InterruptedException {
+ responseIq.setType(IQ.Type.error);
+ PacketError error = new PacketError(condition, type);
+ responseIq.setError(error);
+
+ outQueue.put(responseIq);
+ }
+}
diff --git a/src/test/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStoreTest.java b/src/test/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStoreTest.java
index 3863445a..cfa9d3ac 100644
--- a/src/test/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStoreTest.java
+++ b/src/test/java/org/buddycloud/channelserver/db/jdbc/JDBCNodeStoreTest.java
@@ -19,9 +19,12 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import junit.framework.Assert;
+
import org.apache.commons.collections.CollectionUtils;
import org.buddycloud.channelserver.db.ClosableIteratorImpl;
import org.buddycloud.channelserver.db.CloseableIterator;
@@ -2164,6 +2167,98 @@ public void testCanGetFirehostItemCountWithPrivateItemsAsAdmin()
store.setNodeConf(TEST_SERVER2_NODE1_ID, remoteNodeConf);
assertEquals(7, store.getFirehoseItemCount(true));
}
+
+ @Test
+ public void testOnlySeeSearchResultsFromSubscribedPostsNodes() throws Exception {
+ dbTester.loadData("search-test-1");
+ CloseableIterator items = store.performSearch(
+ new JID("user1@server1"), new ArrayList(), new JID("author@server1"), 1, 25
+ );
+ int counter = 0;
+ while (items.hasNext()) {
+ ++counter;
+ NodeItem item = items.next();
+ assertEquals("a1", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ }
+ assertEquals(1, counter);
+ }
+
+ @Test
+ public void testOnlySeeSearchResultsFromRequestedAuthor() throws Exception {
+ dbTester.loadData("search-test-2");
+ CloseableIterator items = store.performSearch(
+ new JID("user1@server1"), new ArrayList(), new JID("author@server1"), 1, 25
+ );
+ int counter = 0;
+ NodeItem item;
+ while (items.hasNext()) {
+ ++counter;
+ item = items.next();
+ if (1 == counter) {
+ assertEquals("b1", item.getId());
+ assertEquals("/users/another-subscribed@server1/posts", item.getNodeId());
+ } else if (2 == counter) {
+ assertEquals("a1", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ }
+ }
+ assertEquals(2, counter);
+ }
+
+ @Test
+ public void testOnlySeeSearchResultsWithSpecificContent() throws Exception {
+ dbTester.loadData("search-test-3");
+
+ ArrayList searchTerms = new ArrayList();
+ searchTerms.add("keyword");
+ searchTerms.add("post");
+
+ CloseableIterator items = store.performSearch(
+ new JID("user1@server1"), searchTerms, new JID("author@server1"), 1, 25
+ );
+ int counter = 0;
+ NodeItem item;
+ while (items.hasNext()) {
+ ++counter;
+ item = items.next();
+ if (1 == counter) {
+ assertEquals("a3", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ } else if (2 == counter) {
+ assertEquals("a1", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ }
+ }
+ assertEquals(2, counter);
+ }
+
+ @Test
+ public void testOnlySeeSearchResultsWithSpecificContentAndAuthor() throws Exception {
+ dbTester.loadData("search-test-4");
+
+ ArrayList searchTerms = new ArrayList();
+ searchTerms.add("keyword");
+ searchTerms.add("post");
+
+ CloseableIterator items = store.performSearch(
+ new JID("user1@server1"), searchTerms, new JID("author@server1"), 1, 25
+ );
+ int counter = 0;
+ NodeItem item;
+ while (items.hasNext()) {
+ ++counter;
+ item = items.next();
+ if (1 == counter) {
+ assertEquals("a3", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ } else if (2 == counter) {
+ assertEquals("a1", item.getId());
+ assertEquals("/users/subscribed@server1/posts", item.getNodeId());
+ }
+ }
+ assertEquals(2, counter);
+ }
@Test
public void testBeginTransaction() throws Exception {
diff --git a/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGetTest.java b/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGetTest.java
new file mode 100644
index 00000000..d81df66f
--- /dev/null
+++ b/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchGetTest.java
@@ -0,0 +1,316 @@
+package org.buddycloud.channelserver.packetprocessor.iq.namespace.search;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.Assert;
+
+import org.buddycloud.channelserver.channel.ChannelManager;
+import org.buddycloud.channelserver.db.ClosableIteratorImpl;
+import org.buddycloud.channelserver.db.CloseableIterator;
+import org.buddycloud.channelserver.db.exception.NodeStoreException;
+import org.buddycloud.channelserver.packetHandler.iq.IQTestHandler;
+import org.buddycloud.channelserver.packetprocessor.iq.namespace.pubsub.JabberPubsub;
+import org.buddycloud.channelserver.pubsub.accessmodel.AccessModels;
+import org.buddycloud.channelserver.pubsub.affiliation.Affiliations;
+import org.buddycloud.channelserver.pubsub.model.NodeItem;
+import org.buddycloud.channelserver.pubsub.model.NodeSubscription;
+import org.buddycloud.channelserver.pubsub.model.impl.NodeAffiliationImpl;
+import org.buddycloud.channelserver.pubsub.model.impl.NodeItemImpl;
+import org.buddycloud.channelserver.pubsub.model.impl.NodeSubscriptionImpl;
+import org.buddycloud.channelserver.pubsub.subscription.Subscriptions;
+import org.buddycloud.channelserver.utils.node.NodeAclRefuseReason;
+import org.buddycloud.channelserver.utils.node.NodeViewAcl;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+import org.dom4j.tree.BaseElement;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.xmpp.forms.DataForm;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.JID;
+import org.xmpp.packet.Packet;
+import org.xmpp.packet.PacketError;
+import org.xmpp.resultsetmanagement.ResultSetImpl;
+
+public class SearchGetTest extends IQTestHandler {
+
+ private IQ request;
+ private Element element;
+ private BlockingQueue queue = new LinkedBlockingQueue();
+
+ private ChannelManager channelManager;
+
+ private SearchGet search;
+ private JID sender;
+ private JID receiver;
+
+ @Before
+ public void setUp() throws Exception {
+
+ queue = new LinkedBlockingQueue();
+ channelManager = Mockito.mock(ChannelManager.class);
+
+ search = new SearchGet(queue, channelManager);
+
+ sender = new JID("channels.shakespeare.lit");
+ receiver = new JID("romeo@shakespeare.lit/home");
+
+ request = new IQ();
+ request.setFrom(receiver);
+ request.setType(IQ.Type.get);
+ request.setTo(sender);
+ Element query = request.getElement().addElement("query");
+ query.addNamespace("", Search.NAMESPACE_URI);
+
+ Mockito.when(channelManager.isLocalJID(Mockito.any(JID.class)))
+ .thenReturn(true);
+ }
+
+ @Test
+ public void testOnlyAcceptsPacketsFromLocalUsers() throws Exception {
+
+ Mockito.when(channelManager.isLocalJID(Mockito.any(JID.class)))
+ .thenReturn(false);
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.cancel, error.getType());
+ Assert.assertEquals(PacketError.Condition.not_allowed,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsQueryChildElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element query = response.getElement().element("query");
+ Assert.assertNotNull(query);
+
+ Assert.assertEquals(Search.NAMESPACE_URI, query.attributeValue("xmlns"));
+ }
+
+ @Test
+ public void testReturnsInstructionsElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ String instructions = response.getElement()
+ .element("query")
+ .elementText("instructions");
+ Assert.assertEquals(SearchGet.INSTRUCTIONS, instructions);
+ }
+
+
+ @Test
+ public void testReturnsDataFormElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element x = response.getElement()
+ .element("query")
+ .element("x");
+ Assert.assertNotNull(x);
+ Assert.assertEquals(DataForm.NAMESPACE, x.attributeValue("xmlns"));
+ }
+
+ @Test
+ public void testReturnsDataFormTitleElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ String title = response.getElement()
+ .element("query")
+ .element("x")
+ .elementText("title");
+ Assert.assertNotNull(title);
+ Assert.assertEquals(SearchGet.TITLE, title);
+ }
+
+ @Test
+ public void testReturnsDataFormInstructionsElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ String instructions = response.getElement()
+ .element("query")
+ .element("x")
+ .elementText("instructions");
+ Assert.assertNotNull(instructions);
+ Assert.assertEquals(SearchGet.INSTRUCTIONS, instructions);
+ }
+
+ @Test
+ public void testReturnsDataFormTypeElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element formType = (Element) response.getElement()
+ .element("query")
+ .element("x")
+ .elements("field").get(0);
+ Assert.assertEquals(Search.NAMESPACE_URI, formType.elementText("value"));
+ Assert.assertEquals("hidden", formType.attributeValue("type"));
+ Assert.assertEquals("FORM_TYPE", formType.attributeValue("var"));
+ }
+
+ @Test
+ public void testReturnsDataFormContentElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element formType = (Element) response.getElement()
+ .element("query")
+ .element("x")
+ .elements("field").get(1);
+ Assert.assertEquals("text-multi", formType.attributeValue("type"));
+ Assert.assertEquals("content", formType.attributeValue("var"));
+ Assert.assertEquals(SearchGet.CONTENT_FIELD_LABEL, formType.attributeValue("label"));
+
+ }
+
+ @Test
+ public void testReturnsDataFormAuthorElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element formType = (Element) response.getElement()
+ .element("query")
+ .element("x")
+ .elements("field").get(2);
+ Assert.assertEquals("jid-single", formType.attributeValue("type"));
+ Assert.assertEquals("author", formType.attributeValue("var"));
+ Assert.assertEquals(SearchGet.AUTHOR_FIELD_LABEL, formType.attributeValue("label"));
+ }
+
+ @Test
+ public void testReturnsDataFormResultsPerPageElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element formType = (Element) response.getElement()
+ .element("query")
+ .element("x")
+ .elements("field").get(3);
+ Assert.assertEquals("fixed", formType.attributeValue("type"));
+ Assert.assertEquals("rpp", formType.attributeValue("var"));
+ Assert.assertEquals(SearchGet.RPP_FIELD_LABEL, formType.attributeValue("label"));
+
+ }
+
+ @Test
+ public void testReturnsDataFormPageElement() throws Exception {
+
+ search.process(request);
+
+ Assert.assertEquals(1, queue.size());
+
+ IQ response = (IQ) queue.poll();
+ Assert.assertNull(response.getError());
+
+ Assert.assertEquals(receiver, response.getTo());
+ Assert.assertEquals(sender, response.getFrom());
+ Assert.assertEquals(IQ.Type.result, response.getType());
+
+ Element formType = (Element) response.getElement()
+ .element("query")
+ .element("x")
+ .elements("field").get(4);
+ Assert.assertEquals("fixed", formType.attributeValue("type"));
+ Assert.assertEquals("page", formType.attributeValue("var"));
+ Assert.assertEquals(SearchGet.PAGE_FIELD_LABEL, formType.attributeValue("label"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSetTest.java b/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSetTest.java
new file mode 100644
index 00000000..38cd07fb
--- /dev/null
+++ b/src/test/java/org/buddycloud/channelserver/packetprocessor/iq/namespace/search/SearchSetTest.java
@@ -0,0 +1,480 @@
+package org.buddycloud.channelserver.packetprocessor.iq.namespace.search;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import junit.framework.Assert;
+
+import org.buddycloud.channelserver.channel.ChannelManager;
+import org.buddycloud.channelserver.db.ClosableIteratorImpl;
+import org.buddycloud.channelserver.db.CloseableIterator;
+import org.buddycloud.channelserver.db.exception.NodeStoreException;
+import org.buddycloud.channelserver.packetHandler.iq.IQTestHandler;
+import org.buddycloud.channelserver.pubsub.model.NodeItem;
+import org.buddycloud.channelserver.pubsub.model.impl.NodeItemImpl;
+import org.dom4j.Element;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.xmpp.forms.DataForm;
+import org.xmpp.packet.IQ;
+import org.xmpp.packet.JID;
+import org.xmpp.packet.Packet;
+import org.xmpp.packet.PacketError;
+
+public class SearchSetTest extends IQTestHandler {
+
+ private IQ request;
+ private Element element;
+ private BlockingQueue queue = new LinkedBlockingQueue();
+
+ private ChannelManager channelManager;
+
+ private SearchSet search;
+ private JID sender;
+ private JID receiver;
+ private IQ setStanza;
+
+ private String nodeItemNodeId1 = "/users/romeo@montague.lit/home";
+ private String nodeItemNodeId2 = "/users/julet@capulet.lit/home";
+ private String nodeItemId1 = "5w382609806986536982502859083409";
+ private String nodeItemId2 = "fg455g542hg4hhtfgh4554hg5g5g54h4F";
+
+ @Before
+ public void setUp() throws Exception {
+
+ queue = new LinkedBlockingQueue();
+ channelManager = Mockito.mock(ChannelManager.class);
+
+ search = new SearchSet(queue, channelManager);
+
+ sender = new JID("channels.shakespeare.lit");
+ receiver = new JID("romeo@shakespeare.lit/home");
+
+ request = new IQ();
+ request.setFrom(receiver);
+ request.setType(IQ.Type.set);
+ request.setTo(sender);
+ Element query = request.getElement().addElement("query");
+ query.addNamespace("", Search.NAMESPACE_URI);
+
+ Mockito.when(channelManager.isLocalJID(Mockito.any(JID.class)))
+ .thenReturn(true);
+
+ setStanza = readStanzaAsIq("/iq/search/set.stanza");
+ }
+
+ @Test
+ public void testOnlyAcceptsPacketsFromLocalUsers() throws Exception {
+
+ Mockito.when(channelManager.isLocalJID(Mockito.any(JID.class)))
+ .thenReturn(false);
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.cancel, error.getType());
+ Assert.assertEquals(PacketError.Condition.not_allowed,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfDataFormAbsent() throws Exception {
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfNamespaceIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", "some:other:namespace");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfTypeIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "wrongtype");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfFieldFormTypeIsIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "NOT_FORM_TYPE");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfFieldFormTypeValueIsIncorrect()
+ throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText("not:search:type");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfTooFewFields() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(DataForm.NAMESPACE);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "page");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfContentValueIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "content");
+ singleField.addElement("value");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfAuthorValueIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "author");
+ singleField.addElement("value");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfAuthorValueInvalid() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "author");
+ singleField.addElement("value").setText("user@broken@jid.com");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfPageValueIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "page");
+ singleField.addElement("value").setText("sausages");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorIfRppValueIncorrect() throws Exception {
+ Element query = request.getElement().element("query");
+ Element x = query.addElement("x");
+ x.addAttribute("xmlns", DataForm.NAMESPACE);
+ x.addAttribute("type", "submit");
+
+ Element field = x.addElement("field");
+ field.addAttribute("var", "FORM_TYPE");
+ field.addElement("value").addText(Search.NAMESPACE_URI);
+
+ Element singleField = x.addElement("field");
+ singleField.addAttribute("var", "rpp");
+ singleField.addElement("value").setText("bananas");
+
+ search.process(request);
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.modify, error.getType());
+ Assert.assertEquals(PacketError.Condition.bad_request,
+ error.getCondition());
+ }
+
+ @Test
+ public void testReturnsErrorOnChannelManagerException() throws Exception {
+ Mockito.when(
+ channelManager.performSearch(Mockito.any(JID.class),
+ Mockito.any(List.class), Mockito.any(JID.class),
+ Mockito.anyInt(), Mockito.anyInt())).thenThrow(
+ new NodeStoreException());
+
+ search.process(setStanza);
+
+ Packet response = queue.poll();
+ PacketError error = response.getError();
+ Assert.assertNotNull(error);
+ Assert.assertEquals(PacketError.Type.wait, error.getType());
+ Assert.assertEquals(PacketError.Condition.internal_server_error,
+ error.getCondition());
+ }
+
+ @Test
+ public void testNoResultsReturnsExpectedStanza() throws Exception {
+ NodeItem[] items = new NodeItem[0];
+ CloseableIterator itemList = new ClosableIteratorImpl(
+ Arrays.asList(items).iterator());
+
+ Mockito.doReturn(itemList)
+ .when(channelManager)
+ .performSearch(Mockito.any(JID.class), Mockito.any(List.class),
+ Mockito.any(JID.class), Mockito.anyInt(), Mockito.anyInt());
+
+ search.process(setStanza);
+
+ Packet response = queue.poll();
+ Element query = response.getElement().element("query");
+ Assert.assertNotNull(query);
+ Assert.assertEquals(Search.NAMESPACE_URI, query.attributeValue("xmlns"));
+ Assert.assertEquals(0, query.elements().size());
+ }
+
+ @Test
+ public void testReturnsDataInExpectedFormat() throws Exception {
+ NodeItemImpl item1 = new NodeItemImpl(nodeItemNodeId1, nodeItemId1,
+ new Date(), "");
+ NodeItemImpl item2 = new NodeItemImpl(nodeItemNodeId2, nodeItemId2,
+ new Date(), "");
+
+ NodeItem[] itemArray = new NodeItem[2];
+ itemArray[0] = item1;
+ itemArray[1] = item2;
+
+ CloseableIterator itemList = new ClosableIteratorImpl(
+ Arrays.asList(itemArray).iterator());
+
+ Mockito.doReturn(itemList)
+ .when(channelManager)
+ .performSearch(Mockito.any(JID.class), Mockito.any(List.class),
+ Mockito.any(JID.class), Mockito.anyInt(), Mockito.anyInt());
+
+ search.process(setStanza);
+
+ Packet response = queue.poll();
+ Element query = response.getElement().element("query");
+ Assert.assertNotNull(query);
+
+ Element x = query.element("x");
+ Assert.assertNotNull(x);
+
+ Element field = x.element("field");
+ Assert.assertNotNull(field);
+ Assert.assertEquals("FORM_TYPE", field.attributeValue("var"));
+ Assert.assertEquals(Search.NAMESPACE_URI, field.element("value")
+ .getText());
+
+ Element reported = x.element("reported");
+ Assert.assertNotNull(reported);
+
+ List fields = reported.elements("field");
+ Assert.assertEquals(3, fields.size());
+
+ Assert.assertEquals("node", fields.get(0).attributeValue("var"));
+ Assert.assertEquals("Node", fields.get(0).attributeValue("label"));
+ Assert.assertEquals("text-single", fields.get(0).attributeValue("type"));
+
+ Assert.assertEquals("id", fields.get(1).attributeValue("var"));
+ Assert.assertEquals("Item ID", fields.get(1).attributeValue("label"));
+ Assert.assertEquals("text-single", fields.get(1).attributeValue("type"));
+
+ Assert.assertEquals("entry", fields.get(2).attributeValue("var"));
+ Assert.assertEquals("Item", fields.get(2).attributeValue("label"));
+ Assert.assertEquals("xml", fields.get(2)
+ .attributeValue("type"));
+
+ List items = x.elements("item");
+ Assert.assertEquals(2, items.size());
+
+ List itemFields = items.get(0).elements("field");
+ Assert.assertEquals(3, itemFields.size());
+ Assert.assertEquals("node", itemFields.get(0).attributeValue("var"));
+ Assert.assertEquals(nodeItemNodeId1, itemFields.get(0).element("value")
+ .getText());
+
+ Assert.assertEquals("id", itemFields.get(1).attributeValue("var"));
+ Assert.assertEquals(nodeItemId1, itemFields.get(1).element("value")
+ .getText());
+
+ Assert.assertEquals("entry", itemFields.get(2).attributeValue("var"));
+ Assert.assertEquals(1,
+ itemFields.get(2).element("value").elements("entry").size());
+
+ itemFields = items.get(1).elements("field");
+ Assert.assertEquals(3, itemFields.size());
+ Assert.assertEquals("node", itemFields.get(0).attributeValue("var"));
+ Assert.assertEquals(nodeItemNodeId2, itemFields.get(0).element("value")
+ .getText());
+
+ Assert.assertEquals("id", itemFields.get(1).attributeValue("var"));
+ Assert.assertEquals(nodeItemId2, itemFields.get(1).element("value")
+ .getText());
+
+ Assert.assertEquals("entry", itemFields.get(2).attributeValue("var"));
+ Assert.assertEquals(1,
+ itemFields.get(2).element("value").elements("entry2").size());
+ }
+
+ @Test
+ public void testBadlyFormedSourceDataIsIgnored() throws Exception {
+ NodeItemImpl item1 = new NodeItemImpl(nodeItemNodeId1, nodeItemId1,
+ new Date(), "");
+ NodeItemImpl item2 = new NodeItemImpl(nodeItemNodeId2, nodeItemId2,
+ new Date(), "");
+
+ NodeItem[] itemArray = new NodeItem[2];
+ itemArray[0] = item1;
+ itemArray[1] = item2;
+
+ CloseableIterator itemList = new ClosableIteratorImpl(
+ Arrays.asList(itemArray).iterator());
+
+ Mockito.doReturn(itemList)
+ .when(channelManager)
+ .performSearch(Mockito.any(JID.class), Mockito.any(List.class),
+ Mockito.any(JID.class), Mockito.anyInt(), Mockito.anyInt());
+
+ search.process(setStanza);
+
+ Packet response = queue.poll();
+ Element query = response.getElement().element("query");
+
+ Assert.assertNotNull(query);
+
+ Element x = query.element("x");
+ Assert.assertNotNull(x);
+
+ List items = x.elements("item");
+ Assert.assertEquals(1, items.size());
+
+ List itemFields = items.get(0).elements("field");
+ Assert.assertEquals(3, itemFields.size());
+ Assert.assertEquals("node", itemFields.get(0).attributeValue("var"));
+ Assert.assertEquals(nodeItemNodeId2, itemFields.get(0).element("value")
+ .getText());
+
+ Assert.assertEquals("id", itemFields.get(1).attributeValue("var"));
+ Assert.assertEquals(nodeItemId2, itemFields.get(1).element("value")
+ .getText());
+
+ Assert.assertEquals("entry", itemFields.get(2).attributeValue("var"));
+ Assert.assertEquals(1,
+ itemFields.get(2).element("value").elements("entry2").size());
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-1.sql b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-1.sql
new file mode 100644
index 00000000..f86c8ba7
--- /dev/null
+++ b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-1.sql
@@ -0,0 +1,99 @@
+INSERT INTO "nodes" ("node") VALUES ('/users/subscribed@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/unsubscribed@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/pending@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/subscribed@server1/status');
+
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/subscribed@server1/posts', 'user1@server1', 'user1@server1', 'subscribed', current_timestamp - interval '4' second);
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/subscribed@server1/status', 'user2@server1', 'user2@server1', 'subscribed', current_timestamp - interval '3' second);
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/pending@server1/posts', 'user2@server1', 'user2@server1', 'pending', current_timestamp - interval '3' second);
+
+-- The strange order of insertion of the items is deliberate
+
+-- author@server1
+-- not-author@server1
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a1', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/unsubscribed@server1/posts', 'a2', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/pending@server1/posts', 'a3', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/status', 'a4', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
\ No newline at end of file
diff --git a/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-2.sql b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-2.sql
new file mode 100644
index 00000000..f06d38ef
--- /dev/null
+++ b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-2.sql
@@ -0,0 +1,74 @@
+INSERT INTO "nodes" ("node") VALUES ('/users/subscribed@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/another-subscribed@server1/posts');
+
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/subscribed@server1/posts', 'user1@server1', 'user1@server1', 'subscribed', current_timestamp - interval '4' second);
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/another-subscribed@server1/posts', 'user1@server1', 'user1@server1', 'subscribed', current_timestamp - interval '3' second);
+
+-- The strange order of insertion of the items is deliberate
+
+-- author@server1
+-- not-author@server1
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a1', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a2', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ not-author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/another-subscribed@server1/posts', 'b1', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Test 5
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
\ No newline at end of file
diff --git a/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-3.sql b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-3.sql
new file mode 100644
index 00000000..76ef22cd
--- /dev/null
+++ b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-3.sql
@@ -0,0 +1,95 @@
+INSERT INTO "nodes" ("node") VALUES ('/users/subscribed@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/pending@server1/posts');
+
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/subscribed@server1/posts', 'user1@server1', 'user1@server1', 'subscribed', current_timestamp - interval '4' second);
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/pending@server1/posts', 'user1@server1', 'user1@server1', 'pending', current_timestamp - interval '4' second);
+
+-- The strange order of insertion of the items is deliberate
+
+-- author@server1
+-- not-author@server1
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a1', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ A post which contains a certain keyword.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a2', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Only one keyword in this... item
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a3', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ There is also a keyword in this post.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/pending@server1/posts', 'a3', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ There is also a keyword in this post.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
\ No newline at end of file
diff --git a/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-4.sql b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-4.sql
new file mode 100644
index 00000000..f72478b9
--- /dev/null
+++ b/src/test/resources/org/buddycloud/channelserver/testing/jdbc/scripts/search-test-4.sql
@@ -0,0 +1,138 @@
+INSERT INTO "nodes" ("node") VALUES ('/users/subscribed@server1/posts');
+INSERT INTO "nodes" ("node") VALUES ('/users/pending@server1/posts');
+
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/subscribed@server1/posts', 'user1@server1', 'user1@server1', 'subscribed', current_timestamp - interval '4' second);
+INSERT INTO "subscriptions" ("node", "user", "listener", "subscription", "updated")
+ VALUES ('/users/pending@server1/posts', 'user1@server1', 'user1@server1', 'pending', current_timestamp - interval '4' second);
+
+-- The strange order of insertion of the items is deliberate
+
+-- author@server1
+-- not-author@server1
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a1', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ A post which contains a certain keyword.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a2', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ Only one keyword in this... item
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a3', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ There is also a keyword in this post.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/pending@server1/posts', 'a4', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ There is also a keyword in this post.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a4', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ not-author@server1
+ user2@server1
+
+ A post which contains a certain keyword.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
+
+INSERT INTO "items" ("node", "id", "updated", "xml")
+VALUES ('/users/subscribed@server1/posts', 'a5', TIMESTAMP '2010-01-08 11:45:12',
+ '
+ 2010-01-08T11:45:12Z
+
+ author@server1
+ user2@server1
+
+ A contains only one keyword.
+
+ London, England
+ London
+ England
+
+
+ post
+
+ note
+
+ ');
\ No newline at end of file
diff --git a/src/test/resources/stanzas/iq/search/set.stanza b/src/test/resources/stanzas/iq/search/set.stanza
new file mode 100644
index 00000000..899f106b
--- /dev/null
+++ b/src/test/resources/stanzas/iq/search/set.stanza
@@ -0,0 +1,26 @@
+
+
+
+
+ jabber:iq:search
+
+
+ juliet@capulet.lt
+
+
+ dagger
+ poison
+
+
+ 50
+
+
+ 2
+
+
+
+