JCRRepositoryExtension.java

/*
 * Copyright 2021-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.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import java.io.IOException;

/**
 * JUnit 5 (Jupiter) extension that will start an embedded JCR repository before the test method execution and
 * stop the embedded JCR repository when the test method completes.
 *
 * @author <a href="mailto:bmatthews68@gmail.com">Brian Matthews</a>
 * @since 3.0
 */
public class JCRRepositoryExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {

    /**
     * The name of the property used to cache the reference to the JCR repository helper.
     */
    private static final String HELPER = "helper";

    /**
     * The name of the property used to cache the reference to the JCR repository.
     */
    private static final String REPOSITORY = "repository";

    /**
     * This callback is invoked before the test method is executed and is responsible for starting the embedded
     * JCR repository.
     *
     * @param context – the extension context for the Executable about to be invoked; never {@code null}.
     */
    @Override
    public void beforeTestExecution(final ExtensionContext context) {
        final JCRRepositoryConfiguration annotation = getAnnotation(context);
        if (annotation != null) {
            try {
                final JCRRepositoryTester helper = JCRRepositoryTester.createHelper(annotation);
                final ExtensionContext.Store store = getStore(context);
                store.put(HELPER, helper);
                store.put(REPOSITORY, helper.getRepository());
            } catch (final IOException | RepositoryException e) {
                throw new AssertionError("Failed to launch embedded JCR repository", e);
            }
        }
    }

    /**
     * This callback is invoked after the test method is executed and is responsible for stopping the embedded
     * JCR repository.
     *
     * @param context – the extension context for the Executable about to be invoked; never {@code null}.
     */
    @Override
    public void afterTestExecution(final ExtensionContext context) {
        final ExtensionContext.Store store = getStore(context);
        if (store != null) {
            store.remove(REPOSITORY);
            store.remove(HELPER);
        }
    }

    /**
     * Check the parameter type is {@link JCRRepositoryTester} or {@link Repository}.
     *
     * @param parameterContext The context for the parameter for which an argument should be resolved;
     *                         never {@code null}.
     * @param extensionContext The extension context for the Executable about to be invoked; never {@code null}.
     * @return {@code true} if the parameter type is {@link JCRRepositoryTester} or {@link Repository}.
     * Otherwise, {@code false}.
     */
    @Override
    public boolean supportsParameter(final ParameterContext parameterContext,
                                     final ExtensionContext extensionContext) throws ParameterResolutionException {
        final Class<?> parameterType = parameterContext.getParameter().getType();
        return JCRRepositoryTester.class.equals(parameterType) || Repository.class.equals(parameterType);
    }

    /**
     * Resolve {@link JCRRepositoryTester} parameters.
     *
     * @param parameterContext The context for the parameter for which an argument should be resolved;
     *                         never {@code null}.
     * @param extensionContext –The extension context for the Executable about to be invoked; never {@code null}.
     * @return The resolved parameter.
     * @throws ParameterResolutionException If a conneciton to the directory server could not be established.
     */
    @Override
    public Object resolveParameter(final ParameterContext parameterContext,
                                   final ExtensionContext extensionContext)
            throws ParameterResolutionException {
        final Class<?> parameterType = parameterContext.getParameter().getType();
        if (JCRRepositoryTester.class.equals(parameterType)) {
            return getStore(extensionContext).get(HELPER, JCRRepositoryTester.class);
        } else if (Repository.class.equals(parameterType)) {
            return getStore(extensionContext).get(REPOSITORY, Repository.class);
        } else {
            return null;
        }
    }

    /**
     * Get teh context storage for the method invocation.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     * @return The context store.
     */
    private ExtensionContext.Store getStore(final ExtensionContext extensionContext) {
        final ExtensionContext.Namespace namespace = ExtensionContext.Namespace.create(
                JCRRepositoryExtension.class,
                extensionContext.getRequiredTestMethod());
        return extensionContext.getStore(namespace);
    }

    /**
     * Locate the annotation that specifies the configuration for the content repository. The annotation is
     * sought on the test method declaration before falling back to check the test class.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     * @return The {@code JCRRepositoryConfiguration} annotation if found. Otherwise, {@code null}.
     */
    private JCRRepositoryConfiguration getAnnotation(final ExtensionContext extensionContext) {
        final JCRRepositoryConfiguration annotation = extensionContext.getRequiredTestMethod()
                .getAnnotation(JCRRepositoryConfiguration.class);
        if (annotation == null) {
            return extensionContext.getRequiredTestClass().getAnnotation(JCRRepositoryConfiguration.class);
        } else {
            return annotation;
        }
    }
}