/*
* ModeShape (http://www.modeshape.org)
*
* Licensed 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.
*/
package org.modeshape.jcr;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.common.FixFor;
import org.modeshape.common.util.FileUtil;
import org.modeshape.jcr.ValidateQuery.ValidationBuilder;
import org.modeshape.jcr.api.ValueFactory;
import org.modeshape.jcr.api.index.IndexColumnDefinition;
import org.modeshape.jcr.api.index.IndexDefinition.IndexKind;
import org.modeshape.jcr.api.index.IndexDefinitionTemplate;
import org.modeshape.jcr.api.index.IndexManager;
import org.modeshape.jcr.api.query.Query;
import org.modeshape.jcr.api.query.QueryManager;
import org.modeshape.jcr.api.query.QueryResult;
import org.modeshape.jcr.spi.index.provider.IndexProvider;
public abstract class AbstractIndexProviderTest extends SingleUseAbstractTest {
private static final String CONFIG_FILE = "config/repo-config-persistent-local-provider-no-indexes.json";
private static final String STORAGE_DIR = "target/persistent_repository";
@Before
@Override
public void beforeEach() throws Exception {
// We're using a Repository configuration that persists content, so clean it up ...
TestingUtil.waitUntilFolderCleanedUp(storageDir());
// Now start the repository ...
startRepositoryWithConfiguration(repositoryConfiguration());
printMessage("Started repository...");
}
protected InputStream repositoryConfiguration() {
return resource(CONFIG_FILE);
}
protected String storageDir() {
return STORAGE_DIR;
}
@After
@Override
public void afterEach() throws Exception {
super.afterEach();
FileUtil.delete(storageDir());
}
@Test
public void shouldAllowRegisteringNewIndexDefinitionWithSingleStringColumn() throws Exception {
String indexName = "descriptionIndex";
registerValueIndex(indexName, "mix:title", "Index for the 'jcr:title' property on mix:title", "*", "jcr:title",
PropertyType.STRING);
waitForIndexes();
indexManager().unregisterIndexes(indexName);
waitForIndexes();
}
@Test
public void shouldUseSingleColumnStringIndexInQueryAgainstSameNodeType() throws Exception {
registerValueIndex("descriptionIndex", "mix:title", "Index for the 'jcr:title' property on mix:title", "*", "jcr:title",
PropertyType.STRING);
// print = true;
// Add a node that uses this type ...
Node root = session().getRootNode();
Node book1 = root.addNode("myFirstBook");
book1.addMixin("mix:title");
book1.setProperty("jcr:title", "The Title");
Node book2 = root.addNode("mySecondBook");
book2.addMixin("mix:title");
book2.setProperty("jcr:title", "A Different Title");
// Create a node that is not a 'mix:title' and therefore won't be included in the SELECT clauses ...
Node other = root.addNode("somethingElse");
other.setProperty("propA", "a value for property A");
other.setProperty("jcr:title", "The Title");
waitForIndexes();
session.save();
waitForIndexes();
// Compute a query plan that should use this index ...
Query query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] = 'The Title'");
validateQuery().rowCount(1L).useIndex("descriptionIndex").validate(query, query.execute());
// Compute a query plan that should NOT use this index ...
query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE 'The Title'");
validateQuery().rowCount(1L).useNoIndexes().validate(query, query.execute());
// Compute a query plan that should use this index ...
query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE 'The %'");
validateQuery().rowCount(1L).useNoIndexes().validate(query, query.execute());
// Compute a query plan that should use this index ...
query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE '% Title'");
validateQuery().rowCount(2L).useNoIndexes().validate(query, query.execute());
// Compute a query plan that should use this index ...
query = jcrSql2Query("SELECT * FROM [mix:title]");
validateQuery().rowCount(2L).useNoIndexes().validate(query, query.execute());
}
@Test
@FixFor( "MODE-2473" )
public void shouldSkipEntireBatches() throws Exception {
registerNodeTypes("cnd/authors.cnd");
registerValueIndex("baseNodes", "nt:base", null, "*", "jcr:primaryType", PropertyType.NAME);
int batchSize = batchSize() + 1;
for (int i = 0; i < batchSize; i++) {
Node node = session.getRootNode().addNode("node_" + i, "my:content");
node.setProperty("content", "content_" + i);
node.setProperty("author", "author_" + i);
}
Node node = session.getRootNode().addNode("lastNode", "my:content");
node.setProperty("content", "lastContent");
node.setProperty("author", "lastAuthor");
session.save();
waitForIndexes();
int totalNodesCount = batchSize + 1;
NodeIterator nodeIterator = null;
int[] positionsToSkip = new int[] {batchSize - 2, batchSize - 1, batchSize};
List<Node> collectedNodes = new ArrayList<>();
for (int positionToSkip : positionsToSkip ) {
collectedNodes.clear();
final String sql = "select content.[jcr:path] from [my:content] as content order by content.author";
final QueryManager queryManager = session.getWorkspace().getQueryManager();
final Query query = queryManager.createQuery(sql, Query.JCR_SQL2);
final QueryResult result = query.execute();
nodeIterator = result.getNodes();
assertEquals(batchSize + 1, nodeIterator.getSize());
nodeIterator.skip(positionToSkip);
while (nodeIterator.hasNext()) {
collectedNodes.add(nodeIterator.nextNode());
}
assertEquals(collectedNodes.size(), totalNodesCount - positionToSkip);
}
// the last skip should produce just 1 node
Node lastNode = collectedNodes.get(collectedNodes.size() - 1);
assertEquals("lastContent", lastNode.getProperty("content").getString());
assertNotNull("lastAuthor", lastNode.getProperty("author").getString());
}
@Test
@FixFor( "MODE-2682" )
public void shouldUseEnumeratedIndexes() throws Exception {
registerNodeTypes("cnd/images.cnd");
registerEnumeratedIndex("imageFormat", "image:metadata", null, "*", "image:formatName", PropertyType.STRING);
List<String> formats = Arrays.asList("JPEG", "GIF", "PNG", "BMP", "PCX", "IFF", "RAS", "PBM", "PGM", "PPM", "PSD");
int nodeCountPerFormat = 2;
formats.forEach(format -> {
try {
for (int i = 0; i < nodeCountPerFormat; i++) {
Node image = session.getRootNode().addNode("image_" + i, "image:metadata");
image.setProperty("image:formatName", format);
}
} catch (RepositoryException e) {
throw new RuntimeException(e);
}
});
session.save();
waitForIndexes();
formats.forEach(format -> {
try {
Query query = jcrSql2Query("select image.[jcr:path] from [image:metadata] as image where image.[image:formatName] = '" + format + "'");
validateQuery().rowCount(nodeCountPerFormat).useIndex("imageFormat").validate(query, query.execute());
} catch (RepositoryException e) {
throw new RuntimeException(e);
}
});
}
@Override
protected void startRepository() throws Exception {
startRepositoryWithConfiguration(repositoryConfiguration());
}
protected ValueFactory valueFactory() throws RepositoryException {
return session.getValueFactory();
}
protected void registerNodeType( String typeName ) throws RepositoryException {
registerNodeType(typeName, true, false, "nt:unstructured");
}
protected void registerNodeType( String typeName, boolean queryable, boolean mixin, String...declaredSuperTypes) throws RepositoryException {
NodeTypeManager mgr = session.getWorkspace().getNodeTypeManager();
// Create a template for the node type ...
NodeTypeTemplate type = mgr.createNodeTypeTemplate();
type.setName(typeName);
type.setDeclaredSuperTypeNames(declaredSuperTypes);
type.setAbstract(false);
type.setOrderableChildNodes(true);
type.setMixin(mixin);
type.setQueryable(queryable);
mgr.registerNodeType(type, true);
}
protected void registerValueIndex( String indexName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
String propertyName,
int propertyType ) throws RepositoryException {
registerIndex(indexName, IndexKind.VALUE, providerName(), indexedNodeType, desc, workspaceNamePattern, propertyName,
propertyType);
}
protected void registerEnumeratedIndex(String indexName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
String propertyName,
int propertyType) throws RepositoryException {
registerIndex(indexName, IndexKind.ENUMERATED_VALUE, providerName(), indexedNodeType, desc, workspaceNamePattern, propertyName,
propertyType);
}
protected void registerNodeTypeIndex( String indexName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
String propertyName,
int propertyType ) throws RepositoryException {
registerIndex(indexName, IndexKind.NODE_TYPE, providerName(), indexedNodeType, desc, workspaceNamePattern, propertyName,
propertyType);
}
protected void registerTextIndex( String indexName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
String propertyName,
int propertyType ) throws RepositoryException {
registerIndex(indexName, IndexKind.TEXT, providerName(), indexedNodeType, desc, workspaceNamePattern, propertyName,
propertyType);
}
protected void registerIndex( String indexName,
IndexKind kind,
String providerName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
String propertyName,
int propertyType ) throws RepositoryException {
// Create the index template ...
IndexDefinitionTemplate template = indexManager().createIndexDefinitionTemplate();
template.setName(indexName);
template.setKind(kind);
template.setNodeTypeName(indexedNodeType);
template.setProviderName(providerName);
template.setSynchronous(useSynchronousIndexes());
if (workspaceNamePattern != null) {
template.setWorkspaceNamePattern(workspaceNamePattern);
} else {
template.setAllWorkspaces();
}
if (desc != null) {
template.setDescription(desc);
}
// Set up the columns ...
IndexColumnDefinition colDefn = indexManager().createIndexColumnDefinitionTemplate().setPropertyName(propertyName)
.setColumnType(propertyType);
template.setColumnDefinitions(colDefn);
// Register the index ...
indexManager().registerIndex(template, false);
}
protected void registerValueIndex( String indexName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
Map<String, Integer> properties ) throws RepositoryException {
registerIndex(indexName, IndexKind.VALUE, providerName(), indexedNodeType, desc, workspaceNamePattern, properties);
}
protected void registerIndex( String indexName,
IndexKind kind,
String providerName,
String indexedNodeType,
String desc,
String workspaceNamePattern,
Map<String, Integer> properties) throws RepositoryException {
// Create the index template ...
IndexDefinitionTemplate template = indexManager().createIndexDefinitionTemplate();
template.setName(indexName);
template.setKind(kind);
template.setNodeTypeName(indexedNodeType);
template.setProviderName(providerName);
template.setSynchronous(useSynchronousIndexes());
if (workspaceNamePattern != null) {
template.setWorkspaceNamePattern(workspaceNamePattern);
} else {
template.setAllWorkspaces();
}
if (desc != null) {
template.setDescription(desc);
}
List<IndexColumnDefinition> colDefs = new ArrayList<>(properties.size());
for (Map.Entry<String, Integer> entry : properties.entrySet()) {
colDefs.add(indexManager().createIndexColumnDefinitionTemplate().setPropertyName(entry.getKey())
.setColumnType(entry.getValue()));
}
template.setColumnDefinitions(colDefs);
// Register the index ...
indexManager().registerIndex(template, false);
}
protected IndexManager indexManager() throws RepositoryException {
return session().getWorkspace().getIndexManager();
}
protected Query jcrSql2Query( String expr ) throws RepositoryException {
return jcrSql2Query(session(), expr);
}
protected Query jcrSql2Query( JcrSession session,
String expr ) throws RepositoryException {
return session.getWorkspace().getQueryManager().createQuery(expr, Query.JCR_SQL2);
}
protected ValidationBuilder validateQuery() {
return ValidateQuery.validateQuery().printDetail(print);
}
protected int batchSize() {
return IndexProvider.DEFAULT_BATCH_SIZE;
}
protected abstract boolean useSynchronousIndexes();
protected abstract String providerName();
protected void waitForIndexes( long extraTime ) throws InterruptedException {
if (useSynchronousIndexes()) {
Thread.sleep(100L);
} else {
Thread.sleep(1000L);
}
if (extraTime > 0L) Thread.sleep(extraTime);
}
protected void waitForIndexes() throws InterruptedException {
waitForIndexes(0L);
}
}