/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* $Id: IndexedSearchTest.java 512591 2007-02-28 03:35:44Z vgritsenko $
*/
package org.apache.xindice.integration.client.services;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xindice.integration.client.AbstractXmlDbClientTest;
import org.apache.xindice.integration.client.XmlDbClientSetup;
import org.apache.xindice.xml.NodeSource;
import org.apache.xindice.xml.TextWriter;
import org.apache.xindice.xml.dom.NodeImpl;
import org.custommonkey.xmlunit.XMLAssert;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.XPathQueryService;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
/**
* Implements test cases for testing functionality of
* indexed xpath searching.
*
* @version $Revision: 512591 $, $Date: 2007-02-27 22:35:44 -0500 (Tue, 27 Feb 2007) $
* @author Terry Rosenbaum
*/
public class IndexedSearchTest extends AbstractXmlDbClientTest {
private static final Log itsLog = LogFactory.getLog(IndexedSearchTest.class);
/**
* path of collection we use (and reuse to avoid document insertion overhead for each test case)
*/
private static final String PARENT_COLLECTION_PATH = XmlDbClientSetup.INSTANCE_NAME + "/" + XmlDbClientSetup.TEST_COLLECTION_NAME;
private static final String COLLECTION_NAME = "indexedsearch";
private static final String COLLECTION_PATH = PARENT_COLLECTION_PATH + "/" + COLLECTION_NAME;
private static final String TEST_DOCUMENT_PREFIX = "indexedsearchtestdoc";
private static final DocumentBuilderFactory itsDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
private static boolean docsCreated = false;
private static boolean aCollectionCreated = false;
/**
* Performs setup for each test case.
*/
public void setUp() throws Exception {
super.setUp();
if (!aCollectionCreated) {
try {
this.client.dropCollection(PARENT_COLLECTION_PATH, COLLECTION_NAME);
} catch (Exception anException) {
/*
* ignore problems during drop collection above...
*/
} finally {
this.client.createCollection(PARENT_COLLECTION_PATH, COLLECTION_NAME);
aCollectionCreated = true;
}
}
if (!docsCreated) {
createTestDocs();
docsCreated = true;
}
}
/**
* Creates 250 test documents and inserts them into the test Collection.
*/
private void createTestDocs() throws Exception {
for (int anIndex = 0; anIndex < 250; ++anIndex) {
String aNumber = String.valueOf(anIndex);
String aDocument =
"<?xml version='1.0'?>" +
"<person number='" + aNumber + "'>" +
" <first>Sally</first>" +
" <last>Jones" + aNumber + "</last>" +
" <phone type='home'>555-345-6789</phone>" +
"</person>";
this.client.insertDocument(COLLECTION_PATH, "doc" + String.valueOf(anIndex), aDocument);
}
}
private class IndexerTestDefinition {
public class Result {
private ResourceSet itsResourceSet;
private long itsElapsedTime;
/**
* Creates a new object.
*/
public Result(ResourceSet theResourceSet,
long theElapsedTime) {
itsResourceSet = theResourceSet;
itsElapsedTime = theElapsedTime;
}
/**
* Provides this Result's ResourceSet.
*
* @return the ResourceSet of this result
*/
public ResourceSet getResourceSet() {
return itsResourceSet;
}
/**
* Provides this Result's elapsed time (how long the query took).
*
* @return the elapsed time in milliseconds
*/
public long getElapsedTime() {
return itsElapsedTime;
}
}
private String itsTestQuery;
private String itsTestIndexName;
private String itsTestIndexType;
private String itsTestIndexPattern;
private String[] itsTestDocuments;
private String itsDescription;
private long itsExpectedResourceCount;
private Object[] itsExpectedResources;
private int itsIndexSpeedupFactor;
private boolean itsIndexCreated = false;
private boolean itsTestDocumentsAdded = false;
/**
* Creates a new test definition.
*
* @param theDescription description of the test
* @param theTestQuery xpath query to use in the test
* @param theTestIndexName name of index to use for the test
* @param theTestIndexType type of index to use for the test ("Name" or "Value")
* @param theTestIndexPattern pattern to use to create the test's index
* @param theTestDocuments array of any test-specific source documents to insert for the test (may be null if none)
* @param theExpectedResourceCount number of resources expected as a result of the query (if -1 will not be checked)
* @param theExpectedResources one or more resources to expect (need not be the full list;
* any resources provided will be checked against the query results in order provided;
* if null will not be checked)
*/
public IndexerTestDefinition(String theDescription,
String theTestQuery,
String theTestIndexName,
String theTestIndexType,
String theTestIndexPattern,
int theIndexSpeedupFactor,
String[] theTestDocuments,
long theExpectedResourceCount,
Object[] theExpectedResources) {
itsDescription = theDescription;
itsTestQuery = theTestQuery;
itsTestIndexName = theTestIndexName;
itsTestIndexType = theTestIndexType;
itsTestIndexPattern = theTestIndexPattern;
itsIndexSpeedupFactor = theIndexSpeedupFactor;
itsTestDocuments = theTestDocuments;
itsExpectedResourceCount = theExpectedResourceCount;
itsExpectedResources = theExpectedResources;
}
/**
* Runs the test. If any test failure occurs, a JUnit exception is thrown.
*/
public void runTest() throws Exception {
try {
Result aResult;
addTestDocuments();
aResult = runNonIndexed();
checkResult(aResult);
long aNonIndexedTime = aResult.getElapsedTime();
aResult = runIndexed();
checkResult(aResult);
long anIndexedTime = aResult.getElapsedTime();
itsLog.info(itsDescription +
": Non-indexed time = " + aNonIndexedTime +
" Indexed time = " + anIndexedTime +
" Index speedup:" + (anIndexedTime > 0 ? " " + aNonIndexedTime / anIndexedTime + "X" :
anIndexedTime == 0 ? " >" + aNonIndexedTime + "X" : " (Indexed query not run)"));
if (anIndexedTime * itsIndexSpeedupFactor > aNonIndexedTime) {
fail("Query apparently did not use index" +
" Non-indexed time = " + aNonIndexedTime + " Indexed time = " + anIndexedTime);
}
} finally {
removeTestDocuments();
dropIndex();
}
}
/**
* Runs the test query without an index.
*
* @return a Result containing the query result (ResourceSet) and elapsed time
*/
public Result runNonIndexed() throws Exception {
return runQuery();
}
/**
* Runs the test query with an index.
*
* @return a Result containing the query result (ResourceSet) and elapsed time
*/
public Result runIndexed() throws Exception {
createIndex();
try {
return runQuery();
} finally {
dropIndex();
}
}
/**
* Runs the test query.
*
* @return a Result containing the query result (ResourceSet) and elapsed time
*/
public Result runQuery() throws Exception {
Collection col = IndexedSearchTest.this.client.getCollection(IndexedSearchTest.COLLECTION_PATH);
XPathQueryService xpathservice = (XPathQueryService) col.getService("XPathQueryService", "1.0");
org.apache.xindice.Stopwatch aStopwatch = new org.apache.xindice.Stopwatch("Non-indexed starts-with query", true);
ResourceSet resultSet = xpathservice.query(itsTestQuery);
aStopwatch.stop();
return new Result(resultSet, aStopwatch.elapsed());
}
/**
* Checks the result of a test. Throws an assertion exception
* if any check fails.
*
* @param theResult Result to check
*/
public void checkResult(Result theResult) throws Exception {
assertNotNull("Parameter theResult is null", theResult);
ResourceSet aResourceSet = theResult.getResourceSet();
assertNotNull("theResult contains a null ResourceSet", aResourceSet);
if (itsExpectedResourceCount >= 0) {
assertEquals("Result did not contain expected number of resources",
itsExpectedResourceCount, aResourceSet.getSize());
}
if (itsExpectedResources != null && itsExpectedResources.length > 1) {
for (int anIndex = 0; anIndex < itsExpectedResources.length / 2; anIndex += 2) {
XMLResource aResource = (XMLResource) aResourceSet.getResource(anIndex);
Node aNode = aResource.getContentAsDOM();
int anExpectedSourceDocumentIndex = ((Integer) itsExpectedResources[anIndex]).intValue();
String anExpected = "<?xml version=\"1.0\"?>\n" + addSource((String) itsExpectedResources[anIndex + 1],
TEST_DOCUMENT_PREFIX + anIndex,
IndexedSearchTest.COLLECTION_PATH);
String anActual = TextWriter.toString(aNode);
//itsLog.info(itsDescription);
//itsLog.info("Expected resource " + (anIndex / 2) + ":");
//itsLog.info("Expected:");
//itsLog.info(anExpected);
//itsLog.info("Actual:");
//itsLog.info(anActual);
XMLAssert.assertXMLEqual("While checking target Resource " + anIndex / 2,
anExpected, anActual);
}
}
}
/**
* Adds the xindice source document/collection information to the
* root of the specified XML and rreturns the XML as a string.
*
* @param theXML XML to add source to
* @param theKey document key to add
* @param theCollectionName source collection to add
*/
public String addSource(String theXML, String theKey, String theCollectionName) throws Exception {
DocumentBuilder aBuilder = itsDocumentBuilderFactory.newDocumentBuilder();
Document aDocument = aBuilder.parse(
new InputSource(new StringReader(theXML)));
Element elm = aDocument.getDocumentElement();
final String pfx = "src";
elm.setAttribute(NodeImpl.XMLNS_PREFIX + ":" + pfx, NodeSource.SOURCE_NS);
elm.setAttribute(pfx + ":" + NodeSource.SOURCE_COL,
"/" + COLLECTION_PATH);
elm.setAttribute(pfx + ":" + NodeSource.SOURCE_KEY, theKey);
return TextWriter.toString(elm);
}
/**
* Creates the index used by this test.
*/
public void createIndex() throws Exception {
if (!itsIndexCreated) {
IndexedSearchTest.this.client.createIndexer(COLLECTION_PATH,
"<?xml version='1.0'?><index name='" + itsTestIndexName + "' " +
"class='org.apache.xindice.core.indexer." + itsTestIndexType + "Indexer' " +
"pattern='" + itsTestIndexPattern + "' />");
itsIndexCreated = true;
// Wait for the indexer to do the background indexing
Thread.sleep(1250);
}
}
/**
* Drops the index used by this test.
*/
public void dropIndex() throws Exception {
if (itsIndexCreated) {
IndexedSearchTest.this.client.dropIndexer(COLLECTION_PATH, itsTestIndexName);
itsIndexCreated = false;
}
}
/**
* Adds all test-specific documents supplied for this test to the test collection.
*/
public void addTestDocuments() throws Exception {
if (!itsTestDocumentsAdded && itsTestDocuments != null) {
for (int anIndex = 0; anIndex < itsTestDocuments.length; ++anIndex) {
String aDocumentName = TEST_DOCUMENT_PREFIX + anIndex;
IndexedSearchTest.this.client.insertDocument(
COLLECTION_PATH,
aDocumentName, itsTestDocuments[anIndex]);
}
itsTestDocumentsAdded = true;
}
}
/**
* Removes all test-specific documents supplied for this test from the test collection.
*/
public void removeTestDocuments() throws Exception {
if (itsTestDocumentsAdded && itsTestDocuments != null) {
for (int anIndex = 0; anIndex < itsTestDocuments.length; ++anIndex) {
String aDocumentName = TEST_DOCUMENT_PREFIX + anIndex;
IndexedSearchTest.this.client.removeDocument(
COLLECTION_PATH,
aDocumentName);
}
itsTestDocumentsAdded = false;
}
}
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying a specific Element
* name and no Attribute name (e.g. "phone").
*/
public void testSpecificElementNoAttributeNameIndexer() throws Exception {
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementNoAttributeNameIndexer", // description
"/person/address", // query
"SENA", // index name
"Name", // index type
"address", // index pattern
8, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
1, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1),
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>"
});
aTest.runTest();
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying a specific Element
* name and no Attribute name (e.g. "phone").
*/
public void testSpecificElementNoAttributeNameIndexer2() throws Exception {
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementNoAttributeNameIndexer2", // description
"/person/second[@given]", // query
"SENA", // index name
"Name", // index type
"second", // index pattern
10, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<second surname='no' given='yes'>Wally</second>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
1, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1),
"<second surname='no' given='yes'>Wally</second>"
});
aTest.runTest();
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying both an Element
* name and an Attribute name (e.g. "phone@call").
*/
public void testSpecificElementSpecificAttributeNameIndexer() throws Exception {
// select all phone records having a call attribute
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementSpecificAttributeNameIndexer", // description
"/person/phone[@call]", // query
"SESA", // index name
"Name", // index type
"phone@call", // index pattern
8, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
1, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1),
"<phone call='no' type='work'>555-345-6789</phone>"
});
aTest.runTest();
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying a specific Element
* name and a wildcard Attribute name (e.g. "phone@*").
*/
public void testSpecificElementWildAttributeNameIndexer() throws Exception {
// select resources having a "first" element containing a "surname" attribute that is a child of a "person" element
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementWildAttributeNameIndexer", // description
"/person/second[@surname]", // query
"SEWA", // index name
"Name", // index type
"second@*", // index pattern
6, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<second surname='no' given='yes'>Wally</second>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
1, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1),
"<second surname='no' given='yes'>Wally</second>",
});
aTest.runTest();
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying a wildcard Element
* name and a specific Attribute name (e.g. "*@call").
*/
public void testWildElementSpecificAttributeNameIndexer() throws Exception {
// select resources having any element containing a "surname" attribute that is a child of a "person" element
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testWildElementSpecificAttributeNameIndexer", // description
"//*[@given]", // query
"WESA", // index name
"Name", // index type
"*@given", // index pattern
7, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
2, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1),
"<first surname='no' given='yes'>Sally</first>",
new Integer(1),
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>"
});
aTest.runTest();
}
/**
* Tests the functionality of indexed searches on a Collection
* with a name index that has a pattern specifying a wildcard Element
* name and a wildcard Attribute name (e.g. "*@*"). This kind of index
* is probably not useful.
*/
public void testWildElementWildAttributeNameIndexer() throws Exception {
// select nodes with an attribute named 'given'
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testWildElementWildAttributeNameIndexer", // description
"//*[@given]", // query
"WEWA", // index name
"Name", // index type
"*@*", // index pattern
0, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
2, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1), "<first surname='no' given='yes'>Sally</first>",
new Integer(1), "<number given='no' />"
});
aTest.runTest();
}
/**
* Tests a starts-with search query for an Element value that should be resolvable using indexed searching
* on a value index. (index pattern like "last")
*/
public void testSpecificElementNoAttributeValueIndexedStartsWithSearch()
throws Exception {
// search all records whose last name begins with 'Smi'
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementNoAttributeValueIndexedStartsWithSearch", // description
"//person[starts-with(last, 'Smi')]", // query
"SENA", // index name
"Value", // index type
"last", // index pattern
8, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>Smith</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>",
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>Smithers</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
2, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1), "<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>Smith</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>",
new Integer(2), "<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>Smithers</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
});
aTest.runTest();
}
/**
* Tests a starts-with search query for an attribute value that should be resolvable using indexed searching
* on a value index. (index pattern like "phone@call")
*/
public void testSpecificElementSpecificAttributeValueIndexedStartsWithSearch()
throws Exception {
// search all records whose last name begins with 'Smi'
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testSpecificElementSpecificAttributeValueIndexedStartsWithSearch", // description
"//phone[starts-with(@call, 'n')]", // query
"SESA", // index name
"Value", // index type
"phone@call", // index pattern
6, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
1, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1), "<phone call='no' type='work'>555-345-6789</phone>"
});
aTest.runTest();
}
/**
* Tests a starts-with search query for an attribute value that should be resolvable using indexed searching
* on a value index. (index pattern like "*@call")
*/
public void testWildElementSpecificAttributeValueIndexedStartsWithSearch()
throws Exception {
// search all records whose last name begins with 'Smi'
IndexerTestDefinition aTest = new IndexerTestDefinition(
"testWildElementSpecificAttributeValueIndexedStartsWithSearch", // description
"//phone[starts-with(@call, 'n')]", // query
"SENA", // index name
"Value", // index type
"*@call", // index pattern
6, // indexed query speedup expected (conservative)
new String[] { // test docs specifically for this test
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>555-345-6789</phone>" +
"<address call='no'> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>",
"<?xml version='1.0'?>" +
"<person number3='yes'>" +
"<first surname='no' given='yes'>Sally</first>" +
"<last surname='yes'>aSm</last>" +
"<phone call='no' type='work'>525-345-6789</phone>" +
"<address call='no'> " +
"<street>" +
"<number given='no' />" +
"</street>" +
"</address>" +
"</person>"
},
2, // expected result count
new Object[] { // expected resources to check for (can be empty or partial set)
new Integer(1), "<phone call='no' type='work'>555-345-6789</phone>",
new Integer(1), "<phone call='no' type='work'>525-345-6789</phone>"
});
aTest.runTest();
}
}