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 + + + +