/* * 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); } }