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);
+    }
+}

Reply via email to