This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
The following commit(s) were added to refs/heads/master by this push:
new baa80ed5 SLING-13384: Resource Resolver: add support for tests on
actual JCR implementations (#238)
baa80ed5 is described below
commit baa80ed5aab5294a30ff5aa7c3494be3fb5d9f9a
Author: Julian Reschke <[email protected]>
AuthorDate: Mon Oct 12 13:00:00 2026 +0200
SLING-13384: Resource Resolver: add support for tests on actual JCR
implementations (#238)
Co-authored-by: Claude [[email protected]](mailto:[email protected])
---
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);
+ }
+}