/* * Copyright (C) 2010 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.services.jcr.cluster.functional; import org.exoplatform.common.http.client.HTTPResponse; import org.exoplatform.common.http.client.ModuleException; import org.exoplatform.services.jcr.cluster.BaseClusteringFunctionalTest; import org.exoplatform.services.jcr.cluster.JCRWebdavConnection; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.jcr.query.Query; import javax.xml.namespace.QName; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.events.StartElement; /** * Class contains set of query tests for cluster environment * * @author <a href="mailto:nikolazius@gmail.com">Nikolay Zamosenchuk</a> * @version $Id: WebdavQueryTest.java 34360 2009-07-22 23:58:59Z nzamosenchuk $ * */ public class WebdavQueryTest extends BaseClusteringFunctionalTest { public static final String MIME_TEXT_PLAIN = "text/plain"; public static final String MIME_TEXT_PATCH = "text/x-patch"; public static final String MIME_TEXT_HTML = "text/html"; /** * Delay between adding nodes and querying repository (in ms) */ public static final int SLEEP_BEFORE_QUERY = 6000; /** * Full-text query tests */ public void testFullTextSearch() throws Exception { JCRWebdavConnection conn = getConnection(); // Nodes with some text, with unique words in each one in form of <name><content> Map<String, String> nodes = new HashMap<String, String>(); nodes .put( "JCR_Overview", "A JCR is a type of Object Database tailored to the storage, searching, and retrieval of hierarchical data. The JCR API grew o" + "ut of the needs of content management systems, which require storage of documents and other binary objects with associated me" + "tadata; however, the API is applicable to many additional types of application. In addition to object storage, the JCR provid" + "es: APIs for versioning of data; transactions; observation of changes in data; and import or export of data to XML in a standard way."); nodes .put( "JCR_Structure", "The data in a JCR consists of a tree of Nodes with associated Properties. Data is stored in the Properties, which may hold simple " + "values such as numbers and strings or binary data of arbitrary length. Nodes may optionally have one or more types associated with" + " them which dictate the kinds of properties, number and type of child nodes, and certain behavioral characteristics of the nodes. API"); nodes .put( "JCR_Queries", "A JCR can be queried with XPathQuery, can export portions of its tree to XML in two standard formats and can import hierarchies directly" + " from XML. A JCR may optionally support a standardized form of SQL for queries. The Apache Jackrabbit reference implementation of " + "JCR also supports the integration of the Apache Lucene search engine to give full text searches of data in the repository."); nodes.put("JCR_Impl", "eXo Platform JCR implementation on the company wiki. eXo Platform 2 article on theserverside"); // add nodes for (Entry<String, String> entry : nodes.entrySet()) { conn.addNode(entry.getKey(), entry.getValue().getBytes(), MIME_TEXT_PLAIN); } // wait for indexer to flush volatile index sleep(); // map containing test-case: <SQL query> : <expected nodes> Map<String, String[]> sqlCases = new HashMap<String, String[]>(); sqlCases.put("SELECT * FROM nt:base WHERE CONTAINS(*,'tailored')", new String[]{"JCR_Overview"}); sqlCases.put("SELECT * FROM nt:base WHERE CONTAINS(*,'XPathQuery')", new String[]{"JCR_Queries"}); sqlCases.put("SELECT * FROM nt:resource WHERE CONTAINS(*,'API')", new String[]{"JCR_Structure", "JCR_Overview"}); assertQuery(sqlCases, Query.SQL); // map containing test-case: <XPATH query> : <expected nodes> Map<String, String[]> xpathCases = new HashMap<String, String[]>(); xpathCases.put("//element(*, nt:base)[jcr:contains(.,'tailored')]", new String[]{"JCR_Overview"}); xpathCases.put("//element(*, nt:base)[jcr:contains(.,'XPathQuery')]", new String[]{"JCR_Queries"}); xpathCases.put("//element(*, nt:resource)[jcr:contains(.,'API')]", new String[]{"JCR_Structure", "JCR_Overview"}); assertQuery(xpathCases, Query.XPATH); // remove created nodes for (Entry<String, String> entry : nodes.entrySet()) { conn.removeNode(entry.getKey()); } } /** * Simple test, searching nodes by given path and concrete name. */ public void testPathSearch() throws Exception { String testLocalRootName = "testPathSearch"; JCRWebdavConnection conn = getConnection(); conn.addDir(testLocalRootName); List<String> expected = new ArrayList<String>(); expected.add("exoString"); expected.add("exoBoolean"); expected.add("exoInteger"); expected.add("exoLong"); expected.add("exoFloat"); expected.add("exoDouble"); for (String name : expected) { conn.addNode(testLocalRootName + "/" + name, "_data_".getBytes()); } // wait for indexer to flush volatile index sleep(); // map containing test-case: <SQL query> : <expected nodes> Map<String, String[]> sqlCases = new HashMap<String, String[]>(); sqlCases.put("SELECT * FROM nt:base WHERE jcr:path LIKE '/" + testLocalRootName + "[%]/%' AND NOT jcr:path LIKE '/" + testLocalRootName + "[%]/%/%' ", expected.toArray(new String[expected .size()])); sqlCases.put("SELECT * FROM nt:base WHERE fn:name() = 'exoString'", new String[]{"exoString"}); assertQuery(sqlCases, Query.SQL); // map containing test-case: <XPATH query> : <expected nodes> Map<String, String[]> xpathCases = new HashMap<String, String[]>(); xpathCases.put("/jcr:root/" + testLocalRootName + "/ element(*, nt:base)", expected.toArray(new String[expected .size()])); xpathCases.put("//element(*,nt:file)[fn:name() = 'exoString']", new String[]{"exoString"}); assertQuery(xpathCases, Query.XPATH); conn.removeNode(testLocalRootName); } /** * Test, searching over the repository nodes with concrete value of concrete property. * jcr:mimeType is used for querying purposes. */ public void testPropertyValueSearch() throws Exception { JCRWebdavConnection conn = getConnection(); // Nodes with concrete mimetype in form of <name><content> Map<String, String> nodes = new HashMap<String, String>(); // text/plain nodes.put("TextDescription", MIME_TEXT_PLAIN); nodes.put("SmallNote", MIME_TEXT_PLAIN); nodes.put("CalendarMemo", MIME_TEXT_PLAIN); nodes.put("GetThisDone", MIME_TEXT_PLAIN); // text/patch nodes.put("CriticalPath", MIME_TEXT_PATCH); nodes.put("BrokenPatch", MIME_TEXT_PATCH); // text/html nodes.put("FirstPage", MIME_TEXT_HTML); nodes.put("AboutGateIn", MIME_TEXT_HTML); nodes.put("LicenseAgreement", MIME_TEXT_HTML); nodes.put("HomePage", MIME_TEXT_HTML); nodes.put("StrangePage", MIME_TEXT_HTML); // add nodes for (Entry<String, String> entry : nodes.entrySet()) { conn.addNode(entry.getKey(), "content".getBytes(), entry.getValue()); } // wait for indexer to flush volatile index sleep(); // map containing test-case: <SQL query> : <expected nodes> Map<String, String[]> sqlCases = new HashMap<String, String[]>(); sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType ='" + MIME_TEXT_PLAIN + "'", getNodesByMime(nodes, MIME_TEXT_PLAIN)); sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType ='" + MIME_TEXT_HTML + "'", getNodesByMime(nodes, MIME_TEXT_HTML)); sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType LIKE 'text%'", nodes.keySet().toArray( new String[nodes.size()])); assertQuery(sqlCases, Query.SQL); // map containing test-case: <XPATH query> : <expected nodes> Map<String, String[]> xpathCases = new HashMap<String, String[]>(); xpathCases.put("//element(*,nt:resource)[@jcr:mimeType='" + MIME_TEXT_PLAIN + "']", getNodesByMime(nodes, MIME_TEXT_PLAIN)); xpathCases.put("//element(*,nt:resource)[@jcr:mimeType='" + MIME_TEXT_HTML + "']", getNodesByMime(nodes, MIME_TEXT_HTML)); xpathCases.put("//element(*,nt:resource)[jcr:like(@jcr:mimeType, 'text%')]", nodes.keySet().toArray( new String[nodes.size()])); assertQuery(xpathCases, Query.XPATH); // remove created nodes for (Entry<String, String> entry : nodes.entrySet()) { conn.removeNode(entry.getKey()); } } /** * Performs sequence of queries and asserts received results * * @param conn * @param queryCases * map containing test-case: <query> : <expected nodes> * @param lang * Query.SQL or Query.XPATH * @throws IOException * @throws ModuleException * @throws XMLStreamException * @throws FactoryConfigurationError */ private void assertQuery(Map<String, String[]> queryCases, String lang) throws IOException, ModuleException, XMLStreamException, FactoryConfigurationError { if (lang.equals(Query.SQL) || lang.equals(Query.XPATH)) { for (JCRWebdavConnection connection : getConnections()) { for (Entry<String, String[]> entry : queryCases.entrySet()) { HTTPResponse response = lang.equals(Query.SQL) ? connection.sqlQuery(entry.getKey()) : connection.xpathQuery(entry.getKey()); assertEquals(207, response.getStatusCode()); List<String> found; assertEquals(207, response.getStatusCode()); found = parseNodeNames(response.getData()); assertTrue("Lists are not equals:\n*found:\t" + found + "\n*expected:\t" + Arrays.asList(entry.getValue()), compareLists(Arrays.asList(entry.getValue()), found)); } } } else { fail("Unsupported query language:" + lang); } } /** * Given map nodesMap should contain entry: <nodeName>:<mime-type>, this method returns array with names of * nodes that are only of given mime-type. * * @param nodesMap * @param mime * @return */ private String[] getNodesByMime(Map<String, String> nodesMap, String mime) { List<String> filteredNodes = new ArrayList<String>(); for (Entry<String, String> entry : nodesMap.entrySet()) { if (entry.getValue().equals(mime)) { filteredNodes.add(entry.getKey()); } } return filteredNodes.toArray(new String[filteredNodes.size()]); } /** * returns true if lists are equals (order doesn't matter) * * @param expected * @param found * @return */ private boolean compareLists(Collection<String> expected, Collection<String> found) { if (expected == null || found == null) { return false; } return expected.containsAll(found) && found.containsAll(expected); } /** * Extracts names of nodes from response XML * * @param data * @return * @throws XMLStreamException * @throws FactoryConfigurationError * @throws IOException */ private List<String> parseNodeNames(byte[] data) throws XMLStreamException, FactoryConfigurationError, IOException { // flag, that notifies when parser is inside <D:displayname></D:displayname> boolean displayName = false; //Set<String> nodes = new HashSet<String>(); List<String> nodes = new ArrayList<String>(); InputStream input = new ByteArrayInputStream(data); XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(input); QName name = QName.valueOf("{DAV:}displayname"); try { while (reader.hasNext()) { int eventCode = reader.next(); switch (eventCode) { case StartElement.START_ELEMENT : { // if {DAV:}displayname opening element if (reader.getName().equals(name)) { displayName = true; } break; } case StartElement.CHARACTERS : { if (displayName) { // currently reader is inside <D:displayname>nodeName</D:displayname> // adding name to list if not empty String nodeName = reader.getText(); if (nodeName != null && !nodeName.equals("")) { nodes.add(nodeName); } } break; } default : { displayName = false; break; } } } } finally { reader.close(); input.close(); } return new ArrayList<String>(nodes); } /** * Sleep for SLEEP_BEFORE_QUERY seconds. This is needed because Indexer is asynchronous and * volatile index can be flushed after some time. */ private void sleep() { try { Thread.sleep(SLEEP_BEFORE_QUERY); } catch (InterruptedException e) { } } }