This is an automated email from the ASF dual-hosted git repository. reschke pushed a commit to branch SLING-13384 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit 99d6e7353baae79e6f886ca5a6724328c9e59b55 Author: Julian Reschke <[email protected]> AuthorDate: Fri Oct 9 17:21:44 2026 +0100 SLING-13384: Resource Resolver: add support for tests on actual JCR implementations --- pom.xml | 33 +++ .../impl/mapping/OakTestRepository.java | 296 +++++++++++++++++++++ .../impl/mapping/OakTestRepositoryTest.java | 139 ++++++++++ 3 files changed, 468 insertions(+) diff --git a/pom.xml b/pom.xml index 1b5f9ceb..1c003dca 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,8 @@ <sling.java.version>17</sling.java.version> <site.javadoc.exclude>**.internal.**</site.javadoc.exclude> <project.build.outputTimestamp>1786541492</project.build.outputTimestamp> + <!-- used by the tests that run against an actual (in-memory) Oak repository --> + <oak.version>2.6.0</oak.version> </properties> <dependencies> @@ -186,6 +188,37 @@ <version>5.5.0</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.jackrabbit</groupId> + <artifactId>oak-jcr</artifactId> + <version>${oak.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.jackrabbit</groupId> + <artifactId>oak-lucene</artifactId> + <version>${oak.version}</version> + <scope>test</scope> + </dependency> + <!-- JCR resource provider, for the tests against an actual Oak repository --> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.jcr.resource</artifactId> + <version>3.3.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.jcr.api</artifactId> + <version>2.4.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.classloader</artifactId> + <version>1.4.4</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.testing.osgi-mock.junit5</artifactId> diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepository.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepository.java new file mode 100644 index 00000000..dd80e749 --- /dev/null +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepository.java @@ -0,0 +1,296 @@ +/* + * 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. + */ +package org.apache.sling.resourceresolver.impl.mapping; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ch.qos.logback.classic.Level; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.jcr.Jcr; +import org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate; +import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider; +import org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditorProvider; +import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexEditorProvider; +import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProvider; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore; +import org.apache.jackrabbit.oak.query.QueryEngineSettings; +import org.apache.jackrabbit.oak.spi.commit.Observer; +import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider; +import org.apache.sling.jcr.api.SlingRepository; +import org.slf4j.LoggerFactory; + +/** + * An in-memory Oak repository (with the Lucene index provider) for tests that + * need the real query engine, exposed as a {@link SlingRepository} so that the + * JCR resource provider can be used on top of it. + * <p> + * Lucene indexes on the {@value #ASYNC_LANE} lane are only updated when + * {@link #runAsyncIndexing()} is called, so a test controls exactly when the + * index catches up with the repository (and can therefore test with a stale + * index). + */ +public class OakTestRepository implements AutoCloseable { + + static final String ASYNC_LANE = "async"; + + private static final String ADMIN = "admin"; + + /** + * Log levels while the repository is open: Oak logs a lot at debug level (the + * default when running the tests), and the tests resolve thousands of paths. + */ + private static final Map<String, Level> LOG_LEVELS = Map.of( + "org.apache.jackrabbit", Level.WARN, + "org.apache.lucene", Level.WARN, + "org.apache.sling", Level.INFO); + + private final Map<ch.qos.logback.classic.Logger, Level> previousLogLevels = new HashMap<>(); + + private final MemoryNodeStore nodeStore = new MemoryNodeStore(); + private final LuceneIndexProvider indexProvider = new LuceneIndexProvider(); + private final LuceneIndexEditorProvider indexEditorProvider = new LuceneIndexEditorProvider(); + private final AsyncIndexUpdate asyncIndexUpdate; + private final Repository repository; + private final Session session; + + OakTestRepository() throws RepositoryException { + this(new QueryEngineSettings().getLimitInMemory()); + } + + /** + * @param limitInMemory the maximum number of rows the query engine sorts in + * memory; a query that needs to sort more rows (because no index can sort + * them) fails with an {@link UnsupportedOperationException} + */ + OakTestRepository(long limitInMemory) throws RepositoryException { + LOG_LEVELS.forEach((name, level) -> { + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(name); + previousLogLevels.put(logger, logger.getLevel()); + logger.setLevel(level); + }); + QueryEngineSettings querySettings = new QueryEngineSettings(); + querySettings.setLimitInMemory(limitInMemory); + Oak oak = new Oak(nodeStore); + repository = new Jcr(oak) + .with((QueryIndexProvider) indexProvider) + .with((Observer) indexProvider) + .with(indexEditorProvider) + .with(querySettings) + .createRepository(); + // the default content has an async node counter index as well + asyncIndexUpdate = new AsyncIndexUpdate( + ASYNC_LANE, + nodeStore, + CompositeIndexEditorProvider.compose(List.of(indexEditorProvider, new NodeCounterEditorProvider()))); + session = loginAdmin(null); + session.getWorkspace() + .getNamespaceRegistry() + .registerNamespace("sling", "http://sling.apache.org/jcr/sling/1.0"); + } + + /** + * @return an admin session, used by the tests to create content + */ + Session getSession() { + return session; + } + + /** + * @return the repository as a {@link SlingRepository}; all service logins + * get an admin session + */ + SlingRepository getSlingRepository() { + return new OakSlingRepository(); + } + + /** + * Let the async lane catch up with the current repository state. + */ + void runAsyncIndexing() { + asyncIndexUpdate.run(); + if (asyncIndexUpdate.isFailing()) { + throw new IllegalStateException("async indexing failed"); + } + } + + /** + * Create an async Lucene index for a (multivalued) string property: an + * "is not null" index on the property, plus an ordered function index for + * each of the given expressions (for example {@code first([sling:alias])}), + * so that queries sorting on these expressions are sorted by the index. + */ + void createAsyncLuceneIndex(String indexName, String propertyName, String... orderedExpressions) + throws RepositoryException { + Node index = session.getNode("/oak:index").addNode(indexName, "oak:QueryIndexDefinition"); + index.setProperty("type", "lucene"); + index.setProperty("async", new String[] {ASYNC_LANE}); + index.setProperty("compatVersion", 2L); + index.setProperty("evaluatePathRestrictions", true); + index.setProperty("reindex", true); + Node properties = index.addNode("indexRules", "nt:unstructured") + .addNode("nt:base") + .addNode("properties"); + Node property = properties.addNode("property", "nt:unstructured"); + property.setProperty("name", propertyName); + property.setProperty("propertyIndex", true); + for (int i = 0; i < orderedExpressions.length; i++) { + Node ordered = properties.addNode("ordered" + i, "nt:unstructured"); + ordered.setProperty("function", orderedExpressions[i]); + ordered.setProperty("propertyIndex", true); + ordered.setProperty("ordered", true); + } + session.save(); + } + + /** + * @return whether the query engine supports the given condition (for example, + * the {@code if()} and {@code exists()} functions were added in OAK-12406) + */ + boolean supportsCondition(String condition) throws RepositoryException { + try { + explain("SELECT * FROM [nt:base] WHERE " + condition); + return true; + } catch (InvalidQueryException e) { + return false; + } + } + + /** + * @return the query plan Oak uses for the given JCR-SQL2 query + */ + String explain(String query) throws RepositoryException { + return session.getWorkspace() + .getQueryManager() + .createQuery("explain " + query, Query.JCR_SQL2) + .execute() + .getRows() + .nextRow() + .getValue("plan") + .getString(); + } + + @Override + public void close() { + session.logout(); + asyncIndexUpdate.close(); + ((JackrabbitRepository) repository).shutdown(); + indexProvider.close(); + previousLogLevels.forEach(ch.qos.logback.classic.Logger::setLevel); + } + + private Session loginAdmin(String workspace) throws RepositoryException { + return repository.login(new SimpleCredentials(ADMIN, ADMIN.toCharArray()), workspace); + } + + /** + * Minimal {@link SlingRepository} on top of the Oak repository (there are no + * service users in this repository, so service logins are admin logins). + */ + private class OakSlingRepository implements SlingRepository { + + @Override + public String getDefaultWorkspace() { + return null; + } + + @Override + public Session loginAdministrative(String workspace) throws RepositoryException { + return loginAdmin(workspace); + } + + @Override + public Session loginService(String subServiceName, String workspace) throws RepositoryException { + return loginAdmin(workspace); + } + + @Override + public Session impersonateFromService(String subServiceName, Credentials credentials, String workspace) + throws RepositoryException { + Session admin = loginAdmin(workspace); + try { + return admin.impersonate(credentials); + } finally { + admin.logout(); + } + } + + @Override + public Session login(Credentials credentials, String workspace) throws RepositoryException { + return repository.login(credentials, workspace); + } + + @Override + public Session login(Credentials credentials) throws RepositoryException { + return repository.login(credentials); + } + + @Override + public Session login(String workspace) throws RepositoryException { + return repository.login(workspace); + } + + @Override + public Session login() throws RepositoryException { + return repository.login(); + } + + @Override + public String[] getDescriptorKeys() { + return repository.getDescriptorKeys(); + } + + @Override + public boolean isStandardDescriptor(String key) { + return repository.isStandardDescriptor(key); + } + + @Override + public boolean isSingleValueDescriptor(String key) { + return repository.isSingleValueDescriptor(key); + } + + @Override + public Value getDescriptorValue(String key) { + return repository.getDescriptorValue(key); + } + + @Override + public Value[] getDescriptorValues(String key) { + return repository.getDescriptorValues(key); + } + + @Override + public String getDescriptor(String key) { + return repository.getDescriptor(key); + } + } +} diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepositoryTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepositoryTest.java new file mode 100644 index 00000000..01f3d985 --- /dev/null +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/OakTestRepositoryTest.java @@ -0,0 +1,139 @@ +/* + * 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. + */ +package org.apache.sling.resourceresolver.impl.mapping; + +import javax.jcr.Node; +import javax.jcr.Session; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; +import org.apache.sling.jcr.api.SlingRepository; +import org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProvider; +import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker; +import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator; +import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl; +import org.apache.sling.testing.mock.osgi.MockOsgi; +import org.apache.sling.testing.mock.osgi.junit.OsgiContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; +import org.osgi.framework.ServiceReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Minimal test of {@link OakTestRepository}. + * <p> + * Used to verify/test aspects of Sling behavior on top of a JCR repository. + */ +public class OakTestRepositoryTest { + + private final OsgiContext context = new OsgiContext(); + + private OakTestRepository repository; + + /** the repository is created before, and shut down after, the OSGi services */ + @Rule + public final RuleChain rules = RuleChain.outerRule(new ExternalResource() { + @Override + protected void before() throws Throwable { + repository = new OakTestRepository(); + } + + @Override + protected void after() { + if (repository != null) { + repository.close(); + } + } + }) + .around(context); + + private ResourceResolverFactoryActivator activator; + private ResourceResolver resolver; + + @Before + public void setUp() throws Exception { + startResourceResolver(); + } + + @After + public void tearDown() { + if (resolver != null) { + resolver.close(); + } + // the activator is not a service, so OSGi Mock does not deactivate it on shutdown + if (activator != null) { + MockOsgi.deactivate(activator, context.bundleContext()); + } + } + + @Test + public void simpleNodeAndPropertyCreate() throws Exception { + Session session = repository.getSession(); + Node root = session.getRootNode(); + Node test = root.addNode("test", "nt:unstructured"); + session.save(); + Resource resource = resolver.getResource(test.getPath()); + assertEquals(test.getPath(), resource.getPath()); + test.setProperty("foo", "bar"); + session.save(); + assertEquals("bar", resource.getValueMap().get("foo", String.class)); + } + + // ---------------------------------------------------------------- helpers + + /** + * Register the JCR resource provider (on top of the Oak repository) and the + * resource resolver factory, and wait for the factory to be available. + */ + private void startResourceResolver() throws Exception { + context.registerService(SlingRepository.class, repository.getSlingRepository()); + context.registerInjectActivateService(new JcrResourceProvider()); + context.registerInjectActivateService(new ServiceUserMapperImpl()); + context.registerInjectActivateService(new ResourceAccessSecurityTracker()); + context.registerInjectActivateService(new StringInterpolationProviderImpl()); + activator = context.registerInjectActivateService(new ResourceResolverFactoryActivator(), new HashMap<>()); + openResolver(); + } + + private void openResolver() throws InterruptedException, LoginException { + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); + ServiceReference<ResourceResolverFactory> reference; + + while ((reference = context.bundleContext().getServiceReference(ResourceResolverFactory.class)) == null) { + if (System.currentTimeMillis() > deadline) { + throw new AssertionError("resource resolver factory not (re-)registered"); + } + Thread.sleep(50); + } + ResourceResolverFactory factory = context.bundleContext().getService(reference); + assertNotNull(factory); + resolver = factory.getServiceResourceResolver(null); + } +}
