JCRRepositoryTester.java

/*
 * Copyright 2015-2024 Brian Thomas Matthews
 *
 * 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 com.buralotech.oss.jcrunit;

import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.jcr.Jcr;

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.ValueFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW;
import static javax.jcr.Node.JCR_CONTENT;
import static javax.jcr.Property.JCR_DATA;
import static javax.jcr.Property.JCR_ENCODING;
import static javax.jcr.Property.JCR_MIMETYPE;
import static javax.jcr.nodetype.NodeType.NT_FILE;
import static javax.jcr.nodetype.NodeType.NT_FOLDER;
import static javax.jcr.nodetype.NodeType.NT_RESOURCE;
import static org.junit.Assert.assertTrue;

/**
 * A JUnit Rule to help test applications that use the Java Content Repository API. This rule creates an in-memory
 * content repository using <a href="https://jackrabbit.apache.org/oak/">Jackrabbit Oak</a>.
 *
 * @author <a href="mailto:brian.matthews@buralo.com">Brian Matthews</a>
 * @since 3.0
 */
public final class JCRRepositoryTester {

    /**
     * The default username and password.
     */
    private static final String ADMIN = "admin";

    /**
     * The default credentials.
     */
    private static final Credentials ADMIN_CREDENTIALS = new SimpleCredentials(ADMIN, ADMIN.toCharArray());

    /**
     * The JCR repository.
     */
    private final Repository repository;

    /**
     * The credentials used to authenticate when connecting to the repository.
     */
    private final Credentials credentials;

    /**
     * Initialise the helper state with the repository and credentials.
     *
     * @param credentials The credentials.
     */
    public JCRRepositoryTester(final Repository repository,
                               final Credentials credentials) {
        this.repository = repository;
        this.credentials = credentials;
    }

    /**
     * Create the {@link JCRRepositoryTester} using the username, password and XML files.
     *
     * @param username   The username.
     * @param password   The user's password.
     * @param importXMLs Paths of XML files used to populate the repository.
     * @return A {@link JCRRepositoryTester}.
     * @throws IOException         If there was a problem reading from an XML file.
     * @throws RepositoryException If there was a problem creating repository entries.
     */
    public static JCRRepositoryTester createHelper(final String username,
                                                   final String password,
                                                   final String[] importXMLs)
            throws IOException, RepositoryException {
        final Repository repository = new Jcr(new Oak()).createRepository();
        if (!ADMIN.equals(username)) {
            final Session session = repository.login(ADMIN_CREDENTIALS);
            try {
                ((JackrabbitSession)session).getUserManager().createUser(username, password);
                session.save();
            } finally {
                session.logout();
            }
        }
        final JCRRepositoryTester helper = new JCRRepositoryTester(repository, new SimpleCredentials(username, password.toCharArray()));
        for (final String path : importXMLs) {
            helper.importFromXML(ADMIN_CREDENTIALS, path);
        }
        return helper;
    }

    /**
     * Create the {@link JCRRepositoryTester} using the details from the {@link JCRRepositoryConfiguration} annotation.
     *
     * @param annotation Annotation specifying the username, password and XML files.
     * @return A {@link JCRRepositoryTester}.
     * @throws IOException         If there was a problem reading from an XML file.
     * @throws RepositoryException If there was a problem creating repository entries.
     */
    public static JCRRepositoryTester createHelper(final JCRRepositoryConfiguration annotation)
            throws IOException, RepositoryException {
        return createHelper(annotation.username(), annotation.password(), annotation.importXMLs());
    }

    /**
     * Return the JCR repository.
     *
     * @return The JCR repository.
     */
    public Repository getRepository() {
        return repository;
    }

    /**
     * Create a top-level folder in the repository.
     *
     * @param name The name of the new top-level folder to be created.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester createRootFolder(final String name)
            throws RepositoryException {
        final Session session = repository.login(credentials);
        try {
            final Node parent = session.getRootNode();
            parent.addNode(name, NT_FOLDER);
            session.save();
        } finally {
            session.logout();
        }
        return this;
    }

    /**
     * Create a sub-folder in the repository.
     *
     * @param path The fully qualified path of the parent folder.
     * @param name The name of the new sub-folder to be created.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester createFolder(final String path,
                                            final String name)
            throws RepositoryException {
        final Session session = repository.login(credentials);
        try {
            final Node parent = session.getNode(path);
            parent.addNode(name, NT_FOLDER);
            session.save();
        } finally {
            session.logout();
        }
        return this;
    }

    /**
     * Create a file in the repository.
     *
     * @param path     The fully qualified path of the parent folder.
     * @param name     The name of the file to be created.
     * @param type     The content type of the file.
     * @param encoding The content encoding of the file.
     * @param data     The binary content of the file.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester createFile(final String path,
                                          final String name,
                                          final String type,
                                          final String encoding,
                                          final byte[] data)
            throws IOException, RepositoryException {
        try (final InputStream inputStream = new ByteArrayInputStream(data)) {
            return createFile(path, name, type, encoding, inputStream);
        }
    }

    /**
     * Create a file in the repository.
     *
     * @param path     The fully qualified path of the parent folder.
     * @param name     The name of the file to be created.
     * @param type     The content type of the file.
     * @param encoding The content encoding of the file.
     * @param data     The string content of the file.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester createFile(final String path,
                                          final String name,
                                          final String type,
                                          final String encoding,
                                          final String data)
            throws IOException, RepositoryException {
        try (final InputStream inputStream = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))) {
            return createFile(path, name, type, encoding, inputStream);
        }
    }

    /**
     * Create a file in the repository.
     *
     * @param path        The fully qualified path of the parent folder.
     * @param name        The name of the file to be created.
     * @param type        The content type of the file.
     * @param encoding    The content encoding of the file.
     * @param inputStream The input stream that provides the binary content of the file.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester createFile(final String path,
                                          final String name,
                                          final String type,
                                          final String encoding,
                                          final InputStream inputStream)
            throws RepositoryException {
        final Session session = repository.login(credentials);
        try {
            final ValueFactory valueFactory = session.getValueFactory();
            final Node parent = session.getNode(path);
            final Node file = parent.addNode(name, NT_FILE);
            final Node resource = file.addNode(JCR_CONTENT, NT_RESOURCE);
            resource.setProperty(JCR_MIMETYPE, type);
            resource.setProperty(JCR_ENCODING, encoding);
            resource.setProperty(JCR_DATA, valueFactory.createBinary(inputStream));
            session.save();
        } finally {
            session.logout();
        }
        return this;
    }

    /**
     * Assert that a folder exists in the repository.
     *
     * @param path The fully qualified path of the folder.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester assertFolderExists(final String path) {
        try {
            final Session session = repository.login(credentials);
            try {
                final Node node = session.getNode(path);
                assertTrue(path + " is not a folder", node.isNodeType(NT_FOLDER));
            } finally {
                session.logout();
            }
        } catch (final RepositoryException e) {
            throw new AssertionError(path + " does not exist", e);
        }
        return this;
    }

    /**
     * Assert that a file exists in the repository.
     *
     * @param path The fully qualified path of the file.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     */
    public JCRRepositoryTester assertFileExists(final String path) {
        try {
            final Session session = repository.login(credentials);
            try {
                final Node node = session.getNode(path);
                assertTrue(path + " is not a file", node.isNodeType(NT_FILE));
            } finally {
                session.logout();
            }
        } catch (final RepositoryException e) {
            throw new AssertionError(path + " does not exist", e);
        }
        return this;
    }

    /**
     * Import file and folder nodes into the repository with from an XML resource on the class path.
     *
     * @param path The path of the XML resource on the class path.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     * @throws IOException         If there was a problem reading the XML resource.
     * @throws RepositoryException If there was a problem importing the XML resource.
     */
    public JCRRepositoryTester importFromXML(final String path) throws IOException, RepositoryException {
        return importFromXML(Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
    }

    /**
     * Import file and folder nodes into the repository with from an XML resource on the class path using
     * the specified credentials.
     *
     * @param credentials The credentials to use for the operation.
     * @param path The path of the XML resource on the class path.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     * @throws IOException         If there was a problem reading the XML resource.
     * @throws RepositoryException If there was a problem importing the XML resource.
     */
    public JCRRepositoryTester importFromXML(final Credentials credentials,
                                             final String path)
            throws IOException, RepositoryException {
        return importFromXML(credentials, Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
    }

    /**
     * Import file and folder nodes into the repository with from an XML resource loaded from an input stream.
     *
     * @param inputStream The input stream from which the XML resource can be loaded.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     * @throws IOException         If there was a problem reading the XML resource.
     * @throws RepositoryException If there was a problem importing the XML resource.
     */
    public JCRRepositoryTester importFromXML(final InputStream inputStream) throws IOException, RepositoryException {
        return importFromXML(credentials, inputStream);
    }

    /**
     * Import file and folder nodes into the repository with from an XML resource loaded from an input stream using
     * the specified credentials.
     *
     * @param credentials The credentials to use for the operation.
     * @param inputStream The input stream from which the XML resource can be loaded.
     * @return A reference to <code>this</code> to allow fluent-style chaining of invocations.
     * @throws IOException         If there was a problem reading the XML resource.
     * @throws RepositoryException If there was a problem importing the XML resource.
     */
    public JCRRepositoryTester importFromXML(final Credentials credentials, final InputStream inputStream)
            throws IOException, RepositoryException {
        final Session session = repository.login(credentials);
        try {
            session.importXML("/", inputStream, IMPORT_UUID_COLLISION_THROW);
            session.save();
        } finally {
            session.logout();
        }
        return this;
    }
}